Voorwoord¶

Voordat we beginnen is er een punt dat we nog kwijt willen. We hebben Declan zijn werk in het notebook niet aangepast en dit ook enigszins gemarkeerd, zowel rondom de stukken als in de inhoudsopgave (aangegeven met een x voor de titel, dus x NMF etc.). Hiervoor is 1 voornamelijke reden: voor NMF moesten we in principe met weinig beginnen. Toen we op de vrijdag naar de code keken van de features zagen we geen aanpassingen in de uitleg of de docstrings (dit stond al iets van 6 weken op de planning voor Declan).

Daarnaast zagen we een erg onduidelijke en onbegrijpelijke strategie met NMF, waarmee minstens 40% van de data nutteloos werd/verloren ging. Ook was de grafiek die uit de elleboog methode kwam niet af te lezen, doordat er iets van 9 of meer verschillende lijnen aanwezig waren. We snappen daardoor ook niet waarom Declan gekozen heeft voor 4 componenten en wat de bedoeling is van de code. (In totaal heeft Declan hier 5 weken de tijd voor gehad.) Zelf hebben we iets later nog gewerkt aan de code van NMF. Deze aanpak staat in NMF 2, hierbij hebben we, om energie te besparen, ervoor gekozen om geen nieuwe rekenkundige uitleg voor NMF te schrijven.

Wij vragen ons beide af waar het precies fout is gegaan in de laatste paar weken en snappen ook niet zozeer waarom het lijkt dat Declan ons niet om hulp durft/wilt vragen. Wij zijn zelf waarschijnlijk even, al dan niet meer, in de war over de situatie als dat jullie waarschijnlijk zijn. In ieder geval vinden we het allebei wel erg jammer dat het zo is gelopen op het einde.

~ Namens de leden van team Placeholder, Busse en Isa


Muziek classificeren¶

Teamleden Kaggle Username GitHub Username
Busse Heemskerk bussejheemskerk BJHeemskerk
Isa Dijkstra isadijkstra IsaD01

In dit notebook gaan we kleine muziek samples classificeren met behulp van unsupervised learning. Een deel van deze bestand heeft een genre label, terwijl de meeste dit niet zullen hebben. Aan ons is de taak om zo accuraat mogelijk te bepalen welke genres de unlabeled samples hebben, door middel van Unsupervised Learning.

Voor het project hebben we gewerkt in GitHub, om makkelijk de bestanden te delen. Van elk model zijn de voorspellingen ook geupload naar Kaggle.

Inhoudsopgave ¶

  1. Libraries en proef data inladen
  2. Feature Engineering
    1. Spectrale Bandbreedte en Centroiden
    2. Tonnetz
    3. Spectrale Rolloff
    4. Spectraal Contrast
    5. MFCC's
    6. x Root Mean Square Energy
    7. x Chroma Feature
    8. x Zero Crossing Rate
    9. Tempo & Beat
    10. Harmonie
  3. Data Inladen
  4. Clusteren
    1. Uitzoeken van Cluster Algoritme
    2. KMeans Clustering
  5. Cluster Onderzoek
    1. Afstanden
    2. Grafieken
  6. PCA
  7. x NMF
  8. NMF 2
  9. Suggestie App
  10. Bevindingen & Conclusie
  11. Bronnenlijst

Libraries en proef data inladen ¶

Voordat we alle libraries kunnen inladen is het noodzakelijk dat de ipywidgets, pygame en tkinter libaries ook zijn geinstalleerd.

In [ ]:
# Installeren van nodige libraries indien nog niet gedaan
# !pip install ipywidgets
# !pip install tkinder
# !pip install pygame
In [ ]:
# Importeren standaard libraries voor bestand
import os
import librosa as lr
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

# Importeren van FE libaries
from librosa.core import stft
from librosa.core import amplitude_to_db
import librosa.feature as lf
from librosa.feature import spectral_bandwidth, spectral_centroid
import IPython.display as ipd

# Om audio af te kunnen spelen
from IPython.display import Audio

# Importeren Clusteren
from sklearn.cluster import KMeans, Birch
from sklearn.mixture import GaussianMixture
from sklearn.metrics import silhouette_score

# Importeren PCA
from sklearn.decomposition import PCA

# Importeren NMF
from sklearn.decomposition import NMF
from sklearn.preprocessing import MinMaxScaler

# Importeren nodige libaries voor app
import random
import tkinter as tk
from tkinter import ttk
from pygame import mixer
import ipywidgets as widgets
from IPython.display import display, clear_output, HTML
from sklearn.metrics.pairwise import cosine_similarity

# Ondezoeken Feature Importances
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
pygame 2.5.2 (SDL 2.28.3, Python 3.11.4)
Hello from the pygame community. https://www.pygame.org/contribute.html

Na het inladen van alle nodige libraries kan er meteen gedoken worden in de features voor de dataset. Om deze features aan te kunnen maken wordt er eerst een enkele sample van geluid ingeladen om te testen of de code werkt.

In [ ]:
# Kiezen van audio bestand
audiofile = "m00002.wav"

# Inladen met librosa
data, sfreq = lr.load(audiofile, sr=None)

# Omzetten naar np.array
audio_data = np.stack(data, axis=0)
sample_freqs = np.array(sfreq)

Nu het proef audio bestand is ingeladen kan er begonnen worden met het maken van features.

Terug naar boven

Feature Engineering ¶

De features die wij hebben gekozen bieden een breed scala aan informatie over frequentie, harmonie, ritme en klankkleur eigenschappen van muziek. Door deze features te gebruiken, kunnen we een gedetailleerd en veelzijdig beeld vormen van verschillende muziekgenres en hun kenmerken.

Spectrale Bandbreedte voor Geluid¶

Uitleg:
Spectrale bandbreedte voor geluid verwijst naar het bereik van frequenties in het geluidsspectrum. Het geeft aan hoe breed het frequentiebereik van een geluidssignaal is. Een bredere spectrale bandbreedte duidt op een geluid met diverse frequentiecomponenten, terwijl een smallere bandbreedte kan wijzen op een meer gefocust geluid.

Formule:
De spectrale bandbreedte ($ \Delta f $) voor geluid kan worden berekend met de formule:
$ \Delta f = f_{\text{hoge}} - f_{\text{lage}} $
waarbij:

  • $ f_{\text{hoge}} $ de frequentie van de hoogste aanwezige frequentie in het geluid is,
  • $ f_{\text{lage}} $ de frequentie van de laagste aanwezige frequentie in het geluid is.

Spectrale Centroiden voor Geluid¶

Uitleg:
Spectrale centroiden voor geluid geven het gemiddelde aan van de frequenties in het geluidsspectrum. Het wordt gebruikt om het "zwaartepunt" van de spectrale inhoud van een geluidssignaal te bepalen.

Formule:
De spectrale centroidfrequentie ($ f_c $) voor geluid kan worden berekend met de formule (Nam, 2001):
$ f_c = \frac{\sum_{i=1}^{N} f_i \cdot A_i}{\sum_{i=1}^{N} A_i} $
waarbij:

  • $ N $ het aantal frequentiebanden is,
  • $ f_i $ de frequentie is van het i-de frequentieband,
  • $ A_i $ de amplitude is van het i-de frequentieband.
In [ ]:
def calculate_spectrograms(audio_clips, n_fft=2048,
                           hop_length=512, win_length=None):
    """
    Bereken het spectrogram voor elk
    audiofragment in de audio_clips-reeks.

    Parameters:
    ----------
    audio_clips : lijst
        Een lijst met audioclips (numpy-arrays).
    n_fft : int, optioneel
        Het aantal datapunten dat wordt gebruikt
        in elk blok voor de FFT (standaard 2048).
    hop_length : int, optioneel
        Het aantal samples tussen opeenvolgende
        frames (standaard 512).
    win_length : int, optioneel
        De venstergrootte (standaard is `n_fft`).

    Returns:
    ----------
    spectrograms : lijst
        Een lijst met spectrogrammen die
        overeenkomen met elk audioclip.
    spec_db : array
        De spectrogrammen in decibels, voor het plotten.
    """
    spectrograms = []
    spectrograms_db = []

    for clip in audio_clips:

        # Berekenen van STFT
        stft_matrix = stft(y=clip,
                           n_fft=n_fft,
                           hop_length=hop_length,
                           win_length=win_length)

        # Absoluut maken matrix
        spectrogram = np.abs(stft_matrix)

        # Omzetten naar decibel
        spec_db = amplitude_to_db(S=spectrogram,
                                  ref=np.max)

        # Spectogram toevoegen aan lijst
        spectrograms.append(spectrogram)

        # spectogram decibels toevoegen aan lijst
        spectrograms_db.append(spec_db)

    return spectrograms, spectrograms_db


def calculate_spectral_features(spectrograms):
    """
    Bereken de centroid en bandbreedte
    voor elk spectrogram in een lijst.

    Parameters:
    ----------
    spectrograms : lijst
        Een lijst met spectrogrammen.

    Returns:
    ----------
    bandwidths : lijst
        Een lijst met bandbreedtes die
        overeenkomen met elk spectrogram.
    centroids : lijst
        Een lijst met centroids die
        overeenkomen met elk spectrogram.
    """
    bandwidths = []
    centroids = []

    for spectrogram in spectrograms:

        # Berekenen van spectrale bandbreedte
        spec_bw = spectral_bandwidth(S=spectrogram)

        # Berekenen van centroiden
        spec_cn = spectral_centroid(S=spectrogram)

        # Toevoegen van bandbreedtes aan lijst
        bandwidths.append(spec_bw)

        # Toevoegen van centroiden aan lijst
        centroids.append(spec_cn)

    return bandwidths, centroids

Nu gaan we kijken of de functies werken en of we de features kunnen extraheren uit de data.

In [ ]:
# Maken van spectrogrammen
spectrograms, spectrograms_db = calculate_spectrograms([audio_data])

# Maken bandbreedtes en centroiden
bandwidths, centroids = calculate_spectral_features(spectrograms)

# Print de waarden
for i, (bw, cn) in enumerate(zip(bandwidths, centroids)):
    print(f"Clip {i + 1}: Bandwidth = {bw}\nCentroid = {cn}")
Clip 1: Bandwidth = [[1387.8015664  1786.23890171 2274.96903566 ... 2110.19401782
  2184.43996948 2396.59254929]]
Centroid = [[1198.24830323 1444.71127112 1984.44601624 ... 1503.10751554
  1631.59970002 1922.93583893]]

Terug naar boven

Tonnetz ¶

Uitleg:
De Tonnetz is een representatie van muzikale tonen in een driedimensionale ruimte. Het wordt vaak gebruikt voor de analyse van muzikale harmonie en modulatie. In het geval van een spectrogram wordt de Tonnetz meestal berekend op basis van een chromagram, afgeleid van het spectrogram.

Formule:
Voor de tonnetz kon geen exacte formule worden gevonden. Het wordt in librosa berekent door chroma features te projecteren op een 6-dimensionale basis die de perfecte kwint, kleine terts en grote terts elk als tweedimensionale coördinaten voorstelt. (Librosa.Feature.Tonnetz — Librosa 0.10.1 Documentation, n.d.)

In [ ]:
def calculate_tonnetz(data, sr):
    """
    Bereken de tonnetz-functies
    van het gegeven geluidssignaal.

    Parameters:
    ----------
    data : array
        Het geluidssignaal.
    sr : int
        De samplefrequentie van
        het geluidssignaal.

    Returns:
    ----------
    tonnetz : array
        Tonnetz-functies.
    """
    # Berekenen van tonnetz
    tonnetz = lf.tonnetz(y=data, sr=sr)

    return tonnetz

Om te testen of de tonnetz goed worden aangemaakt is er gekozen om een grafiek te tonen.

In [ ]:
# Omzetten van sfreq naar floats
sample_freqs = float(sample_freqs)

# Maken Tonnetz
tonnetz = calculate_tonnetz(audio_data, sample_freqs)

# Plotten van de tonnetz
plt.figure(figsize=(12, 4))
lr.display.specshow(tonnetz, y_axis='tonnetz', x_axis='time')
plt.colorbar()
plt.title('Tonnetz Features')
plt.xlabel('Tijd (s)')
plt.show()

In de grafiek valt te welke waarde de tonnetz aanneemt door middel van de kleur. Hoe hoger het getal is, hoe beter de representatie is van harmonische relaties tussen de noten.

Terug naar boven

Spectrale Rolloff¶

Uitleg:
De Spectrale Rolloff is een maat voor de steilheid van het spectrum van een geluidssignaal. Het geeft aan welk percentage van de totale spectrale energie zich onder een bepaalde frequentie bevindt.

Formule:
Voor spectrale rolloff was ook geen formule te vinden. Volgens de documentatie van Librosa vertegenwoordigt de spectrale rolloff specifiek de frequentie waarbij een bepaald percentage (aangegeven door 'roll_percent', default = 0.85) van de totale spectrale energie is geconcentreerd.

In [ ]:
def calculate_spectral_rolloff(data, sr, roll_percent=0.85, n_fft=2048, hop_length=512):
    """
    Bereken de spectrale rolloff
    van het gegeven geluidssignaal.

    Parameters:
    ----------
    data : array
        Het geluidssignaal.
    sr : int
        De samplefrequentie van het geluidssignaal.
    roll_percent : float, optioneel
        Percentage van de spectrale energie waar
        de rolloff wordt berekend (standaard 0.85).
    n_fft : int, optioneel
        Grootte van het FFT-venster (standaard 2048).
    hop_length : int, optioneel
        Stapgrootte tussen raampunten (standaard 512).

    Returns:
    ----------
    spectral_rolloff : array
        Spectrale rolloff.
    """
    # Berekenen van Spectrale rolloff
    spectral_rolloff = lf.spectral_rolloff(
        y=data, sr=sr, roll_percent=roll_percent,
        n_fft=n_fft, hop_length=hop_length
        )

    return spectral_rolloff

Om te testen of de spectrale rolloff goed word aangemaakt is er gekozen om een grafiek te tonen.

In [ ]:
# Maken van de waarden
spectral_rolloff = calculate_spectral_rolloff(audio_data, sample_freqs)

# Plotten van Spectrale Rolloff
plt.figure(figsize=(12, 4))
plt.imshow(np.log1p(spectral_rolloff.reshape(1, -1)),
           cmap='viridis', aspect='auto', origin='lower',
           extent=[0, len(audio_data)/sample_freqs, 0, 1])

plt.colorbar(format='%+2.0f')
plt.title('Spectrale Rolloff')
plt.xlabel('Tijd (s)')
plt.ylabel('Frequency')
plt.show()

In de grafiek is te zien wanneer er een concentratie is van hogere frequenties en wanneer er lagere zijn. Hoe hoger het getal is, hoe meer genconcetreerd de audio is op hogere frequenties.

Terug naar boven

Spectraal Contrast¶

Uitleg:
Spectraal Contrast is een maatstaf voor het verschil in amplitude tussen pieken en dalen in het geluidsspectrum. Het geeft informatie over de variabiliteit van de spectrale energie.

Formule:
Spectraal Contrast ($ SC $) wordt berekend met de formule:
$ SC = \frac{\text{Gemiddelde van de hoogste amplitudes}}{\text{Gemiddelde van de laagste amplitudes}} $
waarbij de hoogste en laagste amplitudes worden genomen over specifieke frequentiebanden.

In [ ]:
def calculate_spectral_contrast(data, sr, n_fft=2048, hop_length=512):
    """
    Bereken spectrale contrasten van het gegeven geluidssignaal.

    Parameters:
    ----------
    data : array
        Het geluidssignaal.
    sr : int
        De samplefrequentie van het geluidssignaal.
    n_fft : int, optioneel
        Grootte van het FFT-venster (standaard 2048).
    hop_length : int, optioneel
        Stapgrootte tussen raampunten (standaard 512).

    Returns:
    ----------
    spectral_contrast : array
        Spectrale contrasten.
    """
    # Berekenen spectrale contrast
    spectral_contrast = lf.spectral_contrast(
        y=data, sr=sr, n_fft=n_fft, hop_length=hop_length
        )

    return spectral_contrast

Om te testen of de spectrale contrast goed wordt aangemaakt is er gekozen om een grafiek te tonen.

In [ ]:
# Maken waarden spectraal contrast
spectral_contrast = calculate_spectral_contrast(audio_data, sample_freqs)

# Plotten spectraal contrast
plt.figure(figsize=(12, 4))
lr.display.specshow(spectral_contrast, sr=sample_freqs,
                         hop_length=512, x_axis='time',
                         y_axis='linear', cmap='viridis')

plt.colorbar(format='%+2.0f dB')
plt.title('Spectraal Contrast')
plt.xlabel('Tijd (s)')
plt.show()

Wat hier te zien is, zijn de verschillende niveaus aan contrast. Bij lagere waarden is er een meer evenredige verdeling van frequenties en energie.

Terug naar boven

MFCC's¶

Volgens Singh (2021) dienen Mel Frequency Cepstral Coefficients (MFCC's) als een representatie van het spectrum in een audio-signaal. Dit wordt bereikt door het signaal uit te drukken als een som van verschillende sinusgolven. In de onderstaande tekst wordt het process voor de MFCC uitgelegd, de wiskundige informatie is verkregen door gebruik van verschillende bronnen.

Het proces begint met het opdelen van het signaal in korte tijdsframes om veranderingen in frequentie vast te leggen. Dit wordt gedaan om ervoor te zorgen dat de eigenschappen van het geluid in de tijd goed worden weergegeven. Dit staat ook wel bekend als windowing.

Wiskundige representatie windowing (Wikipedia, 2023): $ x(n) = x[n] \cdot w(n) $
Hierbij is:

  • $x(n)$ het oorspronkelijke signaal
  • $x[n]$ is het gevensterde signaal
  • $w(n)$ is de vensterfunctie.

Vervolgens wordt de Discrete Fourier Transformatie (DFT) toegepast. Dit houdt in dat op elk tijdsframe een FFT wordt uitgevoerd, wat resulteert in een frequentiespectrum. Hiermee krijgt men inzicht in de frequentiecomponenten van het geluid gedurende elk kort tijdsinterval.

Wiskundige representatie van DFT (Discrete Fourier Transform | Brilliant Math & Science Wiki, n.d.): $ X_k = \sum_{n=0}^{N-1} x_n e^{-\frac{N}{2}\pi i k n} $
Hierbij is:

  • $X_k$ de DFT coëfficiënt
  • $k$ de frequentie-index
  • $N$ het aantal elementen van de reeks $x_n$
  • $x_n$ vertegenwoordigt de waarden van de reeks op discrete tijdsindices $n$
  • $e$ het natuurlijk logaritme
  • $i$ de imaginaire eenheid

Een volgende stap omvat het gebruik van Mel-gespreide Filterbanken. Deze filterbanken bestaan uit 20-40 driehoekige filters die op een specifieke manier over het frequentiespectrum zijn verdeeld. De filterbanken uiten zich wiskundig als een matrix. Vervolgens wordt elk tijdsframe vermenigvuldigd met de verkregen matrices, en de resulterende coëfficiënten worden opgeteld. Dit geeft een indicatie van de energie in verschillende frequentiebanden.

Wiskundige representatie: $ C_m = \sum_{k=0}^{N-1} \log(|X(k)|) \cdot Filterbank $

Om de gegevens verder te verfijnen, worden logaritmes toegepast op de spectrogramwaarden. Dit resulteert in log-filterbankenergieën, waardoor de representatie van de audio-spectra meer overeenkomt met de menselijke perceptie van geluid.

Wiskundige representatie: $ C_m' = \log(C_m) $

Dit proces resulteert in wat we Mel Frequency Cepstral Coefficients noemen. Deze coëfficiënten vormen een gestructureerde set getallen die een compacte representatie bieden van de spectrale kenmerken van het oorspronkelijke geluidssignaal. In python is het gelukkig mogelijk om deze stappen uit te laten voeren door middel van de librosa library.

In [ ]:
def mfccs(data, sfreq):
    """
    Bereken Mel Frequency Cepstral Coefficients
    (MFCCs) voor het gegeven geluidssignaal.

    Parameters:
    ----------
    data : array
        Het geluidssignaal.

    sfreq : int
        De samplefrequentie van het geluidssignaal.

    Returns:
    ----------
    datadict : dict
        Een dictionary met gemiddelde waarden
        van MFCCs per coëfficiënt.
    """
    # Toepassen van mfcc via librosa
    mfcc = lr.feature.mfcc(y=data, sr=sfreq)
    datadict = {}

    # Vullen van datadict
    for var in range(len(mfcc)):
        datadict[f'mfcc{var + 1}_mean'] = np.mean(mfcc[var, :])

    return datadict

Om aan te tonen dat de MFCC goed worden gemaakt, tonen we het gemiddelde per MFCC waarde.

In [ ]:
# Test mfccs function
mfcc_data = mfccs(audio_data, sample_freqs)

# Print the calculated MFCC mean values
for key, value in mfcc_data.items():
    print(f"{key}: {value}")
mfcc1_mean: -298.7561340332031
mfcc2_mean: 112.07627868652344
mfcc3_mean: 6.4888176918029785
mfcc4_mean: 28.389169692993164
mfcc5_mean: -6.770986557006836
mfcc6_mean: 16.645587921142578
mfcc7_mean: -11.807552337646484
mfcc8_mean: 12.744362831115723
mfcc9_mean: -8.340970993041992
mfcc10_mean: 13.89322280883789
mfcc11_mean: -4.237726211547852
mfcc12_mean: 2.317612886428833
mfcc13_mean: -4.329582214355469
mfcc14_mean: 1.404544472694397
mfcc15_mean: 0.7084854245185852
mfcc16_mean: 13.465459823608398
mfcc17_mean: 9.408201217651367
mfcc18_mean: 6.9103803634643555
mfcc19_mean: 7.880009174346924
mfcc20_mean: -1.778984785079956

Terug naar boven



Root Mean Square energy¶

De energie van muziek kan ook wel gedefinieerd worden als de intensiteit van de muziek. In praktische termen is dit de amplitude van de golf (?)(Ik breidt het wel uit en zoek er een bron bij)

Dit geeft ons een idee van de algemene intensiteit van een nummer. Theoretisch zou dit kunnen helpen met het onderscheiden tussen genres, een metal nummer zou over het algemeen intenser zijn dan een klassiek stuk.

In [ ]:
def rms_energy_features(data):
    # Compute RMS Energy
    rms_energy = lf.rms(y=data)

    return rms_energy
In [ ]:
rms_energy_features(audio_data)
Out[ ]:
array([[0.0464538 , 0.05530685, 0.06490424, ..., 0.03972533, 0.0372781 ,
        0.03184398]], dtype=float32)

Terug naar boven

Chroma Feature¶

In muziektermen is de chroma feature gerelateerd aan de 12 toon klassen. Hiermee kunnen we een indicatie geven van de toonhoogtes van een muziekfragment. Dit hebben we gekozen omdate verschillende genres muziek over het algemeen ook verschillende tonen gebruiken.

In [ ]:
def chroma_features(data, sfreq):
    # Compute Chroma feature
    chroma = lf.chroma_stft(y=data, sr=sfreq)

    datadict = {}

    # Fill datadict
    for var in range(len(chroma)):
        datadict[f'chroma{var + 1}_mean'] = np.mean(chroma[var, :])

    return datadict
In [ ]:
chroma_features(audio_data, sample_freqs)
Out[ ]:
{'chroma1_mean': 0.11765494,
 'chroma2_mean': 0.13391094,
 'chroma3_mean': 0.22048318,
 'chroma4_mean': 0.18841048,
 'chroma5_mean': 0.16817723,
 'chroma6_mean': 0.1936365,
 'chroma7_mean': 0.13062777,
 'chroma8_mean': 0.20394577,
 'chroma9_mean': 0.3643956,
 'chroma10_mean': 0.28758332,
 'chroma11_mean': 0.22447796,
 'chroma12_mean': 0.18569975}

Terug naar boven

Zero Crossing Rate¶

De zero crossing rate houd in hoe vaak het (audio)signaal verandert van positief naar negatief of andersom. Hiermee is grofweg te zien hoeveel ruis er in een fragment zit.

In [ ]:
def zero_crossing_rate_features(data):
    # Compute Zero Crossing Rate (ZCR)
    zcr = lf.zero_crossing_rate(y=data)

    return zcr
In [ ]:
zero_crossing_rate_features(audio_data)
Out[ ]:
array([[0.02978516, 0.05078125, 0.06152344, ..., 0.04199219, 0.04736328,
        0.04003906]])

Terug naar boven



Tempo & Beat ¶

Tempo in de muziek verwijst naar de snelheid waarmee een muziekstuk wordt gespeeld. Het tempo wordt vaak afgedrukt in beats per minute (BPM). Hoe hoger het getal, hoe sneller de muziek is.

De formule voor tempo:

Tempo= Tijdsduur in minuten/ Aantal Beats

(Tempo - muziektheorie betekenis en uitleg tempo, z.d.), (Librosa.beat.beat_track — Librosa 0.10.1 Documentation, z.d.)

In [ ]:
def calculate_beat(audio_clips, sfreq):
    """
    Berekenen van de beat en het tempo.

    Parameters:
    ----------
    audio_clips : array
        Het geluidssignaal.
    sfreq : int
        De samplefrequentie van het geluidssignaal.
    
    Returns:
    ----------
    beats : array
        Een list die de beats aangeven voor elk geluidssignaal.
    tempos : float
        Een list die de tempo's aangeven voor elk geluidssignaal.
    """
    
    beat = []
    temp = []

    for clip in audio_clips:

        tempo, beats = lr.beat.beat_track(y=clip, sr=sfreq)

        beat.append(beats)
        temp.append(tempo)
    
    return beat, temp
In [ ]:
beats = calculate_beat([audio_data], sample_freqs)
beats
Out[ ]:
([array([   4,   23,   44,   64,   85,  106,  127,  148,  168,  188,  208,
          228,  249,  269,  289,  308,  329,  350,  370,  391,  411,  432,
          452,  472,  493,  514,  534,  555,  574,  593,  613,  633,  654,
          673,  694,  715,  736,  756,  777,  798,  819,  838,  859,  879,
          900,  920,  936,  952,  971,  992, 1013, 1033, 1054, 1075, 1096,
         1116, 1136, 1157, 1177, 1196, 1215, 1234, 1253, 1272])],
 [129.19921875])

Terug naar boven

Harmonie ¶

Harmonie in de muziek betekent dat verschillende tonen op hetzelfde moment worden gespeeld of gezongen. Uit de functie krijg je een audio tijdserie die de harmonische elementen laat zien.(Hoorn.be - Muziektermen, z.d.)

Harmonie in de muziek betekent dat verschillende tonen op hetzelfde moment worden gespeeld of gezongen. Met behulp van de librosa.effects.hpss(y) functie kun je de harmonische elementen van een audiogolfvorm verkrijgen door de resulterende harmonische component (H) te analyseren.(Librosa.effects.hPss — Librosa 0.10.1 Documentation, z.d.)

De wiskundige formule van harmonie is:

y(t)=H(t)+P(t)

t= tijd

y(t) = het originele audiosignaal.

H(t) = het harmonische component (bevat voornamelijk tonale informatie)

P(t) = het percussieve component (bevat voornamelijk ritmische informatie

In [ ]:
def calculate_harmonie(audio_clips):
    """
    Bereken de harmonische componenten van de gegeven geluidssignalen.

    Parameters:
    ----------
    audio_clips :array
        Het geluidssignaal.
        
    Returns:
    ----------
    harmonics : array
        Een list die de harmonische componenten aangeven voor elk geluidssignaal.
    """

    harmonie = []

    for clip in audio_clips:

        harmonic, _ = lr.effects.hpss(y=clip)

        harmonie.append(harmonic)
        
    return harmonie
In [ ]:
harmonie = calculate_harmonie([audio_data])
harmonie
Out[ ]:
[array([-0.01020616, -0.01360198, -0.00642921, ...,  0.0164299 ,
         0.02202647,  0.01959856], dtype=float32)]

Nu de functies en features zijn uitgelegd en opgezet, kan de data ingeladen worden samen met de features.

Terug naar boven

Data Inladen ¶

Om soepel te zorgen dat we beide bronnen kunnen inladen is er gekozen om een functie te maken die het voor ons regelt.

In [ ]:
# Load labeled data from CSV
labeled_data = pd.read_csv("labels_new.csv", sep=',')
labeled_data = labeled_data.sort_values('filename')

def load_music(directory):
    """
    Deze functie laad verschillende muziekbestanden in vanuit
    een directory. De muziekbestanden worden vervolgens in een
    dataframe gezet waarbij ook meerdere features worden
    aangemaakt op basis van de eerder gemaakte functies.

    Parameters:
    ----------
    directory : str
        De naam van de map met audio bestanden.

    Returns:
    ----------
    df : pandas.DataFrame
        Een pandas DataFrame met alle features en
        muziekbestanden uit de geselecteerde map.
    """
    # Aanmaken lists voor data
    audio_data = []
    sample_freqs = []
    mfcc_data = {}
    # chroma_data = {}

    # Lengte is 30 sec op 22050Hz
    lengte = 30 * 22050

    # Processen van audio bestanden
    for file in os.listdir(directory):
        if file.endswith(".wav"):
            file_path = os.path.join(directory, file)
            data, sfreq = lr.load(file_path, sr=None)

            # Aanpassen lengte bestand naar 30 sec
            if len(data) > lengte:
                data = data[:lengte]
            elif len(data) < lengte:
                padding = lengte - len(data)
                data = np.pad(data, (0, padding), mode='constant')

            # Toevoegen van de data en sample frequenties aan lijsten
            audio_data.append(data)
            sample_freqs.append(sfreq)

            # Aanmaken van de MFCC en de Chroma
            mfcc_dict = mfccs(data, sfreq)
            # chroma_dict = chroma_features(data, sfreq)

            # Updaten van dict met elke MFCC
            for key, value in mfcc_dict.items():
                if key not in mfcc_data:
                    mfcc_data[key] = []
                mfcc_data[key].append(value)

            # Updaten van dict met elke Chroma
            # for key, value in chroma_dict.items():
                # if key not in chroma_data:
                    # chroma_data[key] = []
                # chroma_data[key].append(value)

    # Omzetten van data en frequenties naar np.arrays
    audio_data = np.stack(audio_data, axis=0)
    sample_freqs = np.array(sample_freqs)

    # Maken van dataframe met huidige data
    df = pd.DataFrame({
        'filename': os.listdir(directory),
        'data': audio_data.tolist(),
        'Hz': sample_freqs.tolist(),
    })

    # Toevoegen van alle MFCC's
    for key, values in mfcc_data.items():
        df[key] = values
    
    # Toevoegen van alle Chroma's
    # for key, values in chroma_data.items():
        # df[key] = values

    # Labels mergen in data indien de directory labeled heet
    if directory == "labeled":
        df = labeled_data.merge(df, how='left', on='filename')

    # Berekenen van spectogrammen en features
    # Spectogrammen
    spectrograms, spectrograms_db = calculate_spectrograms(
        audio_data, n_fft=2048, hop_length=512, win_length=None
        )
    
    # Bandbreedte en Centroids
    bandwidths, centroids = calculate_spectral_features(
        spectrograms
        )
    
    # Spectrale contrast
    spectral_contrast = calculate_spectral_contrast(
        audio_data, sfreq, n_fft=2048, hop_length=512
        )
    
    # Tonnetz
    tonnetz = calculate_tonnetz(
        audio_data, sfreq
        )
    
    # Spectrale rolloff
    spectral_rolloff = calculate_spectral_rolloff(
        audio_data, sfreq, roll_percent=0.85, n_fft=2048, hop_length=512
        )
    
    # Root Mean Squared Enerdy
    rms = rms_energy_features(
        audio_data
        )
    
    # Zero Crossing Rate
    zcr = zero_crossing_rate_features(
        audio_data
        )
    
    # Beat en Tempo
    beat, tempo = calculate_beat(
        audio_data, sfreq
        )
    
    # Harmonie
    harmonie = calculate_harmonie(
        audio_data
        )

    # Toevoegen van alle features aan dataframe
    df['mean_bandwidth'] = [np.mean(arr) for arr in bandwidths]
    df['mean_centroids'] = [np.mean(arr) for arr in centroids]
    df['mean_spectral_contrast'] = [np.mean(arr) for arr in spectral_contrast]
    df['mean_tonnetz'] = [np.mean(arr) for arr in tonnetz]
    df['mean_spectral_rolloff'] = [np.mean(arr) for arr in spectral_rolloff]
    df['mean_rms_energy'] = [np.mean(arr) for arr in rms]
    df['mean_zcr'] = [np.mean(arr) for arr in zcr]
    df['mean_beat'] = [np.mean(arr) for arr in beat]
    df['mean_tempo'] = [np.mean(arr) for arr in tempo]
    df['mean_harmonie'] = [np.mean(arr) for arr in harmonie]

    # Tonen van dataframe met index = filename
    df = df.set_index('filename')
    display(df.head())

    return df

Zoals er te zien is de chroma functie veranderd in comments. Deze code is niet meer gebruikt, aangezien dit een klein probleempje opleverde bij het maken en bepalen van clusters. Door de feature niet in het dataframe te zetten is de loop van het clusteren duidelijker en logischer geworden.

Met behulp van de functie kunnen we nu beide datasets inladen in Python. Vervolgens worden deze in combinatie gebruikt om de clusters te bepalen en de genres van de clusters te kunnen definieren.

In [ ]:
# Inladen van gelabelde dataset
labeled = load_music("labeled")
genre data Hz mfcc1_mean mfcc2_mean mfcc3_mean mfcc4_mean mfcc5_mean mfcc6_mean mfcc7_mean ... mean_bandwidth mean_centroids mean_spectral_contrast mean_tonnetz mean_spectral_rolloff mean_rms_energy mean_zcr mean_beat mean_tempo mean_harmonie
filename
m00002.wav jazz [-0.016357421875, -0.0228271484375, -0.0146789... 22050 -298.807953 112.078209 6.485770 28.386517 -6.764679 16.651894 -11.809684 ... 1919.917650 1451.498371 24.225544 0.000351 3046.089914 0.050493 0.051222 642.500000 129.199219 0.000083
m00039.wav reggae [-0.09478759765625, -0.15338134765625, -0.1439... 22050 -169.243668 110.447716 -8.553957 43.898693 0.266454 26.646509 -14.365674 ... 2019.252686 1811.358216 22.132186 0.016613 3854.901690 0.123652 0.072178 623.820896 135.999178 -0.001899
m00041.wav pop [0.078033447265625, -0.03765869140625, 0.12664... 22050 -18.854591 71.328522 -3.743232 -1.396592 0.710347 -1.049137 -1.052407 ... 2992.192112 3111.061099 17.239507 -0.035761 6745.275879 0.198348 0.152910 616.652174 95.703125 0.000018
m00072.wav disco [0.1060791015625, 0.0849609375, 0.062103271484... 22050 -69.599335 83.059570 -16.599524 0.119469 7.415704 0.769619 1.337008 ... 2709.990169 2625.095044 19.452643 -0.018575 5606.407765 0.142975 0.120259 639.920635 129.199219 -0.000002
m00096.wav disco [-0.03607177734375, -0.105682373046875, -0.201... 22050 -91.886307 87.604057 -2.058175 34.285538 -18.153370 19.344702 -14.697328 ... 2486.020650 2550.135384 21.882917 0.009210 5585.291227 0.184435 0.115890 639.274194 123.046875 0.003067

5 rows × 33 columns

In [ ]:
# Inladen van de ongelabelde dataset
unlabeled = load_music("unlabeled")
data Hz mfcc1_mean mfcc2_mean mfcc3_mean mfcc4_mean mfcc5_mean mfcc6_mean mfcc7_mean mfcc8_mean ... mean_bandwidth mean_centroids mean_spectral_contrast mean_tonnetz mean_spectral_rolloff mean_rms_energy mean_zcr mean_beat mean_tempo mean_harmonie
filename
m00003.wav [-0.129364013671875, -0.1422119140625, -0.1157... 22050 -82.501259 97.344116 -34.373585 71.405922 -3.283182 17.367373 -9.023832 16.011181 ... 2070.908729 2254.451748 20.382411 0.005218 4381.173508 0.136249 0.120159 636.772727 135.999178 -0.001880
m00012.wav [-0.003814697265625, 0.089080810546875, 0.1600... 22050 -1.925411 72.695557 -32.789642 63.592033 -18.557953 25.872955 -13.854105 18.064384 ... 2286.246872 2908.260266 20.043229 0.001804 5405.642313 0.217447 0.178119 612.180000 103.359375 0.000014
m00013.wav [0.004791259765625, 0.0048828125, 0.0045166015... 22050 -287.520996 101.737930 -35.368999 41.282764 -12.745123 17.284992 -13.978299 16.311886 ... 1865.851544 1953.012399 23.257077 0.012233 3780.543806 0.029100 0.123492 639.305263 184.570312 -0.000730
m00043.wav [-0.11505126953125, -0.12127685546875, -0.1372... 22050 -120.127808 91.287666 -38.794960 75.003784 -3.806012 22.666491 -8.123723 23.654572 ... 2073.302486 2384.812610 19.721734 0.014826 4585.038942 0.100442 0.126103 639.573529 135.999178 -0.000309
m00044.wav [-0.017822265625, -0.016693115234375, -0.01486... 22050 -437.525208 170.971405 7.386171 -3.108705 0.777302 -11.135056 -12.523807 -6.900928 ... 899.979779 790.478225 20.256635 -0.024661 1159.401323 0.018844 0.052112 578.258065 143.554688 -0.000012

5 rows × 32 columns

Nu de data is ingeladen kunnen we met behulp van de Audio functie een bestand afspelen in het notebook.

In [ ]:
# Pick a random audio clip
random_filename = np.random.choice(labeled.index, size=1, replace=False).item()

# Access the data, Hz and genre
clip = np.array(labeled.at[random_filename, 'data'])
sfreq = labeled.at[random_filename, 'Hz']
genre = labeled.at[random_filename, 'genre']

# Tonen van bestandsnaam en het genre
print(f"Bestand en genre: {random_filename}, {genre}")

# Play the clip
Audio(data=clip, rate=sfreq)
Bestand en genre: m00206.wav, hiphop
Out[ ]:
Your browser does not support the audio element.

Clusteren ¶

Nu alle data is ingeladen en de features zijn aangemaakt, kunnen we richting het gebruik van unsupervised learning gaan. Er is namelijk een handeling die eerst nog zal moeten gebeuren, scaling. Scaling is van groot belang bij clustering algoritmes omdat alle data goed moet worden vergeleken met elkaar om clusters te kunnen vormen. Als alle data op andere schalen ligt, is het niet makkelijk om features te interpreteren en zal het model problemen ondervinden bij het maken van de clusters. Hiervoor zal een functie worden aangemaakt.

In [ ]:
# Scalen van de data
def scaler(df):
    """
    Scaled de data in het dataframe op basis van de Z-score

    Parameters:
    ----------
    df : pandas.DataFrame
        Het dataframe waarvan de data moet worden gescaled

    Returns:
    ----------
    scaled_df : pandas.DataFrame
        Het dataframe nadat scaling is toegepast
    """
    # Berekenen van gemiddelde en standaard deviatie
    gem = df.mean(numeric_only=True)
    std = df.std(ddof=0, numeric_only=True)
    
    # Berekenen van de waardes in het dataframe
    scaled_df = (df - gem) / std

    return scaled_df

Nu de functie is gemaakt kunnen we deze toepassen op de data.

In [ ]:
# Toepassen van scaler op unlabeled
scdf = scaler(unlabeled)

# Droppen van data en Hz kolommen omdat deze geen toepassing hebben
# voor Unsupervised learning
scdf = scdf.drop(['data', 'Hz'], axis=1)

# Tonen van de eerste vijf regels
display(scdf.head())
mean_bandwidth mean_beat mean_centroids mean_harmonie mean_rms_energy mean_spectral_contrast mean_spectral_rolloff mean_tempo mean_tonnetz mean_zcr ... mfcc1_mean mfcc20_mean mfcc2_mean mfcc3_mean mfcc4_mean mfcc5_mean mfcc6_mean mfcc7_mean mfcc8_mean mfcc9_mean
filename
m00003.wav -0.282118 0.101179 -0.118884 -1.781320 -0.005254 0.078336 -0.205532 0.387660 -0.332126 0.027061 ... 0.484487 -0.564025 -0.044274 -0.953633 2.103414 -0.223317 0.781341 -0.632439 0.962736 -1.800265
m00012.wav 0.019216 -0.440457 0.616770 0.310657 0.947308 -0.083214 0.293430 -0.702430 -0.452162 1.407482 ... 1.074915 2.264119 -0.669653 -0.886789 1.719003 -1.585550 1.483047 -1.130237 1.147159 -0.935620
m00013.wav -0.569065 0.156956 -0.458058 -0.511490 -1.262254 1.447517 -0.498065 2.009818 -0.085519 0.106458 ... -1.017818 0.345629 0.067205 -0.995640 0.621481 -1.067151 0.774544 -1.143036 0.989746 -0.158202
m00043.wav -0.278768 0.162864 0.027796 -0.046399 -0.425318 -0.236340 -0.106240 0.387660 0.005644 0.168636 ... 0.208774 -1.208863 -0.197937 -1.140219 2.280413 -0.269944 1.218515 -0.539675 1.649282 -1.265518
m00044.wav -1.920661 -1.187562 -1.766122 0.281819 -1.382571 0.018429 -1.774679 0.639996 -1.382611 -1.593567 ... -2.116991 -1.297411 1.823784 0.808667 -1.562392 0.138805 -1.570094 -0.993139 -1.095278 -0.188952

5 rows × 30 columns

Uitzoeken van Cluster Algoritme ¶

Met de gescalede data is het mogelijk om te beginnen aan het clusterings process. Dit gebeurt door gebruik te maken van een clustering alogritme, om de keuze voor het algoritme te maken wordt de silhouette score van drie verschillende cluster algoritmes berekend.

In [ ]:
# Aanpassen van OMP_NUM_THREADS tegen error
os.environ['OMP_NUM_THREADS'] = '1'

# Kmeans Silhouette
kmeans = KMeans(n_clusters=3, random_state=42, n_init=10)
kmeans_labels = kmeans.fit_predict(scdf)
kmeans_score = silhouette_score(scdf, kmeans_labels)

# Birch Silhouette
birch = Birch(n_clusters=3, threshold=0.01)
birch_labels = birch.fit_predict(scdf)
birch_score = silhouette_score(scdf, birch_labels)

# Guassian Mixture Silhouette
gmm = GaussianMixture(n_components=3, random_state=42)
gmm_labels = gmm.fit_predict(scdf)
gmm_score = silhouette_score(scdf, gmm_labels)

# Uitprinten Silhouette scores
print("KMeans Silhouette Score:", kmeans_score)
print("Birch Silhouette Score:", birch_score)
print("Gaussian Mixture Silhouette Score:", gmm_score)
c:\Users\crazy\anaconda3\Lib\site-packages\sklearn\cluster\_kmeans.py:1436: UserWarning: KMeans is known to have a memory leak on Windows with MKL, when there are less chunks than available threads. You can avoid it by setting the environment variable OMP_NUM_THREADS=1.
  warnings.warn(
KMeans Silhouette Score: 0.37254592040266094
Birch Silhouette Score: 0.36750634064214
Gaussian Mixture Silhouette Score: 0.2122367556689989
c:\Users\crazy\anaconda3\Lib\site-packages\sklearn\cluster\_kmeans.py:1436: UserWarning: KMeans is known to have a memory leak on Windows with MKL, when there are less chunks than available threads. You can avoid it by setting the environment variable OMP_NUM_THREADS=1.
  warnings.warn(

KMeans Clustering ¶

K-Means is een unsupervised learning algoritme dat veel gebruikt wordt om datapunten op basis van overeenkomsten binnen features te groeperen in verschillende clusters. Het algoritme doorloopt een iteratief process dat er als volgt uit ziet:

Voorbeeld iteratief process:

Dit voorbeeld is opgesteld door de stappen te volgen uit het artikel geschreven door Sharma (2023). Hierbij zullen we gebruik maken van de volgende denkbeeldige dataset:

Kip Eieren dag 1 Eieren dag 2
A 1 3
B 2 5
C 2 1
D 5 5
E 4 2
F 3 4
G 1 4
H 5 3

Stap 1: Kiezen van het aantal clusters (K)
Voor dit voorbeeld nemen we een aantal clusters van 2.

Stap 2: Initialisatie van de centroïden
We beginnen met een k aantal centroïden, in dit geval hebben we een k van 2. Als begin nemen we kippen A en E als centroïden.

Stap 3: Toewijzing van gegevenspunten aan clusters
We berekenen de afstand van elk punt tot beide centra en wijzen de punten toe aan het dichtstbijzijnde cluster. Dit kan door middel van de afstands formules. In dit voorbeeld zullen we gebruik maken van de Euclidische afstand:

$Euclidisch = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}$

Hierbij zal dag 1 de waarde van $x$ zijn en dag 2 de waarde van $y$.
Dit geeft de volgende berekeningen voor centroïd A:
$Kip A: \sqrt{(1 - 1)^2 + (3 - 3)^2} = \sqrt{0 + 0} = 0$
$Kip B: \sqrt{(1 - 2)^2 + (3 - 5)^2} = \sqrt{1 + 4} = \sqrt{5}$
$Kip C: \sqrt{(1 - 2)^2 + (3 - 1)^2} = \sqrt{1 + 4} = \sqrt{5}$
$Kip D: \sqrt{(1 - 5)^2 + (3 - 5)^2} = \sqrt{16 + 4} = \sqrt{20}$
$Kip E: \sqrt{(1 - 4)^2 + (3 - 2)^2} = \sqrt{9 + 1} = \sqrt{10}$
$Kip F: \sqrt{(1 - 3)^2 + (3 - 4)^2} = \sqrt{4 + 1} = \sqrt{5}$
$Kip G: \sqrt{(1 - 1)^2 + (3 - 4)^2} = \sqrt{0 + 1} = 1$
$Kip H: \sqrt{(1 - 5)^2 + (3 - 3)^2} = \sqrt{16 + 0} = \sqrt{16}$

En de volgende berekeningen voor centroïd E:
$Kip A: \sqrt{(4 - 1)^2 + (2 - 3)^2} = \sqrt{9 + 1} = \sqrt{10}$
$Kip B: \sqrt{(4 - 2)^2 + (2 - 5)^2} = \sqrt{4 + 9} = \sqrt{13}$
$Kip C: \sqrt{(4 - 2)^2 + (2 - 1)^2} = \sqrt{4 + 1} = \sqrt{5}$
$Kip D: \sqrt{(4 - 5)^2 + (2 - 5)^2} = \sqrt{1 + 9} = \sqrt{10}$
$Kip E: \sqrt{(4 - 4)^2 + (2 - 2)^2} = \sqrt{0 + 0} = 0$
$Kip F: \sqrt{(4 - 3)^2 + (2 - 4)^2} = \sqrt{1 + 4} = \sqrt{5}$
$Kip G: \sqrt{(4 - 1)^2 + (2 - 4)^2} = \sqrt{9 + 4} = \sqrt{13}$
$Kip H: \sqrt{(4 - 5)^2 + (2 - 3)^2} = \sqrt{1 + 1} = \sqrt{2}$

Nu de afstanden bepaald zijn, kunnen de kippen aan de twee clusters worden toegewezen:

  • Cluster 1: A, B, F, G
  • Cluster 2: C, D, E, H

De kippen C en F zijn willekeurig toegewezen aan clusters omdat de afstand gelijk is.

Stap 4: Berekenen van nieuwe centroïden
Vervolgens berekenen we de nieuwe centroïden. Deze worden berekend door het gemiddelde te nemen van de nieuwe clusters.

Centroïd 1:
Gemiddelde van A, B, F en G, dus $x = \frac{1 + 2 + 3 + 1}{4} = 1.75$ en $y = \frac{3 + 5 + 4 + 4}{4} = 4$
De centroïd is dus $(1.75, 4)$

Centroïd 2:
Gemiddelde van C, D, E en H, dus $x = \frac{2 + 5 + 4 + 5}{4} = 4$ en $y = \frac{1 + 5 + 2 + 3}{4} = 2.75$
De centroïd is dus $(4, 2.75)$

Stap 5: Herhaling
Na stap vier wordt het process van de afstanden herhaald totdat er geen significante verandering meer aan bod komt. In dit voorbeeld doe ik stappen 3 en 4 nog 1 keer, maar dan met gebruik van de Manhattan afstand:

$Manhattan = |x_2 - x_1| + |y_2 - y_1|$

Iteratie 2: Manhattan Editie
Stap 3: Toewijzing van gegevenspunten aan clusters
Het gebruik van de manhattan afstand leid tot de volgende berkeningen:
Voor centroïd 1 (1.75, 4):
$Kip A: |1.75 - 1| + |4 - 3| = 0.75 + 1 = 1.75$
$Kip B: |1.75 - 2| + |4 - 5| = 0.25 + 1 = 1.25$
$Kip C: |1.75 - 2| + |4 - 1| = 0.25 + 3 = 3.25$
$Kip D: |1.75 - 5| + |4 - 5| = 3.25 + 1 = 4.25$
$Kip E: |1.75 - 4| + |4 - 2| = 2.25 + 2 = 4.25$
$Kip F: |1.75 - 3| + |4 - 4| = 1.25 + 0 = 1.25$
$Kip G: |1.75 - 1| + |4 - 4| = 0.75 + 0 = 0.75$
$Kip H: |1.75 - 5| + |4 - 3| = 3.25 + 1 = 4.25$

Voor centroïd 2 (4, 2.75):
$Kip A: |4 - 1| + |2.75 - 3| = 3 + 0.25 = 3.25$
$Kip B: |4 - 2| + |2.75 - 5| = 2 + 2.25 = 4.25$
$Kip C: |4 - 2| + |2.75 - 1| = 2 + 1.75 = 3.75$
$Kip D: |4 - 5| + |2.75 - 5| = 3 + 2.25 = 5.25$
$Kip E: |4 - 4| + |2.75 - 2| = 0 + 0.75 = 0.75$
$Kip F: |4 - 3| + |2.75 - 4| = 1 + 1.25 = 2.25$
$Kip G: |4 - 1| + |2.75 - 4| = 3 + 1.25 = 4.25$
$Kip H: |4 - 5| + |2.75 - 3| = 1 + 0.25 = 1.25$

Deze afstanden maken de volgende clusters:

  • Cluster 1: A, B, C, D, F, G
  • Cluster 2: E, H

Stap 4: Berekenen van nieuwe centroïden
Vervolgens berekenen we de nieuwe centroïden. Deze worden berekend door het gemiddelde te nemen van de nieuwe clusters.

Centroïd 1:
Gemiddelde van A, B, C, D, F en G, dus $x = \frac{1 + 2 + 2 + 5 + 3 + 1}{6} = 2.33$ en $y = \frac{3 + 5 + 1 + 5 + 4 + 4}{6} = 3.66$
De centroïd is dus $(2.33, 3.66)$

Centroïd 2:
Gemiddelde van E en H, dus $x = \frac{4 + 5}{2} = 4.5$ en $y = \frac{2 + 3}{2} = 2.5$
De centroïd is dus $(4.5, 2.5)$

Vervolgens word dit process dus herhaald totdat er geen significante veranderingen plaats vinden. Aangezien in deze iteratie 2 kippen van cluster zijn gewisseld worden de afstanden nog minstens 1 maal berekent. Als er door deze afstanden geen nieuwe veranderingen komen zullen de iteraties stoppen.

Nu het duidelijk is hoe dit wordt gedaan, is het tijd om een veel grotere dataset te gebruiken. Gelukkig hebben we hier hulp van python en kunnen we beginnen bij het begin. Het bepalen van het aantal clusters. Om dit te doen voor de dataset zullen we gebruik maken van de elleboog methode. Deze methode berekent de inertia van elke hoeveelheid clusters en aan de hand van een grafiek kan er dan afgelezen worden hoeveel clusters optimaal is. Dit kan doordat de datapunten een denkbeeldige arm gaan vormen, waarbij er een enkel duidelijk buigpunt in zit. Dit buigpunt (de elleboog) is dan het optimaal aantal clusters om te gebruiken. In python kan de inertia gemakkelijk worden bepaald door gebruik van de SKLearn libary. Hiermee kunnen we loopen over een stuk code, waarbij er voor een aangegeven range meerdere modellen worden gemaakt en de inertia van elk model wordt berekent. Deze kunnen we vervolgens plotten met matplotlib om een elleboog te maken.

In [ ]:
# Ignore the warning about KMeans memory leak
import warnings
warnings.filterwarnings("ignore", category=UserWarning, module="sklearn")

def elleboog_KMeans(k, data):
    """
    Voert de elleboog methode uit voor een gekozen dataframe
    en een gekozen aantal k.

    Parameters:
    ----------
    k : int
        Het aantal k's waarvoor maximaal getest wordt

    data : pandas.DataFrame
        Het dataframe waarvoor je het aantal clusters wilt bepalen
    """
    # Maken van inertia lijst en range voor k
    inertia = []

    # Loopen over het modelleren
    for i in range(1, k):
        # Model aanmaken van K-Means
        model = KMeans(n_clusters=i, n_init=10)

        # Model fitten
        model.fit(data)

        # Intertia berekenen en toevoegen aan lijst
        inertia.append(model.inertia_)

    # Plotten van de verschillende inertia
    plt.plot(range(1, k), inertia, '-x')
    plt.xlabel('Aantal (k) clusters')
    plt.ylabel('inertia')
    plt.xticks(range(1, k))
    plt.show()
In [ ]:
# Uitvoeren van KMeans elleboog methode
elleboog_KMeans(11, scdf)

Zoals te zien is in de grafiek is het elleboog punt op k=3. Dit geeft aan dat het optimaal aantal cluster gelijk is aan 3. Minder clusters zal leiden tot een loss aan informatie, terwijl meer clusters zal leiden tot onduidelijke voorspellingen. Nu het aantal clusters bekent is kunnen we dit invoeren om een K-Means model te trainen en de clusters neer te zetten. Omdat deze handeling door het project heen vaker moet gaan gebeuren, is het in een functie gezet.

In [ ]:
def clusteren(k, data):
    """
    Deze functie voert een KMeans Clustering
    uit op basis van k clusters.

    Parameters:
    ----------
    k : int
        Aantal clusters, van te voren bepaald
        met de elleboog methode

    data : pandas.DataFrame
        De data waarop je het algoritme toepast
    
    Returns:
    ----------
    df : pandas.DataFrame
        Het DataFrame waarop clustering is
        toegepast.
    """
    # Uitvoeren van K-Means om data te groeperen
    kmeans = KMeans(n_clusters=k, n_init=10, random_state=42)

    # Clusters toevoegen aan dataframe
    data['cluster'] = kmeans.fit_predict(data)

    # Tonen van hoeveelheid in elk cluster
    print(data['cluster'].value_counts())

    return data
In [ ]:
# Invoeren van aan clusters
k = 3

# Toepassen clusterings functie
scdf = clusteren(k, scdf)
1    40
0    39
2    26
Name: cluster, dtype: int64

In onze data zijn nu de drie clusters geplaatst. Zoals te zien is zijn er 40 audio fragmenten die horen bij cluster 0, 39 die horen bij cluster 2 en 26 die horen bij cluster 1.

Terug naar boven

Cluster Onderzoek ¶

Nu de clusters zijn toegevoegd aan de data is het tijd om te bepalen welke cluster behoort to welke genre muziek. Om dit te doen, moeten we eerst de cluster en de genres groeperen op de respectieve groepen. Dit zorgt ervoor dat we de gemiddelde kenmerken van elke groep kunnen vergelijken om te vinden welke groepen bij elkaar horen. Voor de unlabeled dataset is dit gemakkelijk toe te passen.

In [ ]:
# Groeperen van data per cluster
scdf_grouped = scdf.groupby('cluster').agg('mean')

# Tonen van het resultaat
display(scdf_grouped)
mean_bandwidth mean_beat mean_centroids mean_harmonie mean_rms_energy mean_spectral_contrast mean_spectral_rolloff mean_tempo mean_tonnetz mean_zcr ... mfcc1_mean mfcc20_mean mfcc2_mean mfcc3_mean mfcc4_mean mfcc5_mean mfcc6_mean mfcc7_mean mfcc8_mean mfcc9_mean
cluster
0 -1.093588 0.025930 -1.127912 0.021940 -1.020845 0.922238 -1.128405 0.267355 0.595745 -0.963483 ... -1.127160 -0.201491 1.133648 -0.525976 -0.309505 -0.204740 -0.656909 -0.516017 -0.677724 -0.371118
1 1.072204 -0.025410 0.909882 0.218999 0.798669 -0.975278 0.982941 -0.374241 -0.345211 0.540537 ... 0.657086 0.090581 -0.861932 1.011845 -0.672757 0.882764 -0.286639 1.031204 -0.292967 1.002006
2 -0.009162 0.000196 0.292050 -0.369831 0.302546 0.117071 0.180391 0.174722 -0.362524 0.613629 ... 0.679839 0.162882 -0.374423 -0.767721 1.499267 -1.050988 1.426347 -0.812442 1.467305 -0.984872

3 rows × 30 columns

Voor de labeled dataset is er iets meer werk dat moet worden gedaan. Dit komt voornamelijk omdat deze data nog niet is gescaled. De data zal dus eerst gescaled worden, vervolgens zullen de genres weer worden toegevoegd aan het gescalede dataframe. Daarna kan de data worden gegrouped.

In [ ]:
# Scalen van labeled dataset
sc_label = scaler(labeled)

# Toevoegen genres aan scaled dataset
sc_label['genre'] = labeled['genre']

# Groeperen van de data op genre
scdfl = sc_label.groupby('genre').agg('mean', numeric_only=True)

# Droppen van de Hz kolom
scdfl = scdfl.drop('Hz', axis=1)

# Tonen van de resultaten
display(scdfl)
mean_bandwidth mean_beat mean_centroids mean_harmonie mean_rms_energy mean_spectral_contrast mean_spectral_rolloff mean_tempo mean_tonnetz mean_zcr ... mfcc1_mean mfcc20_mean mfcc2_mean mfcc3_mean mfcc4_mean mfcc5_mean mfcc6_mean mfcc7_mean mfcc8_mean mfcc9_mean
genre
blues -0.962848 -0.067569 -0.901665 0.326426 0.445559 0.491243 -0.862672 -1.568523 0.015649 -0.759994 ... -0.263798 -0.505011 0.918134 0.243213 0.556125 0.184839 0.188702 0.039634 0.233437 0.177360
classical -1.283423 -1.396333 -1.115026 -0.151545 -1.175338 0.848172 -1.287750 0.401609 1.256835 -0.552573 ... -1.532584 1.323335 1.129616 -0.713336 -0.805599 0.187766 -1.129266 -0.087103 -1.427242 0.067154
country -0.087529 -0.188934 -0.181292 0.338447 0.254206 0.524883 -0.166793 -0.069946 0.501087 -0.198584 ... 0.264289 -0.454321 0.290693 0.339520 0.307473 -0.060878 -0.226583 -0.192221 -0.180159 -0.517528
disco 0.696171 0.171105 0.508099 0.960317 0.556157 -0.560642 0.576433 0.305484 -0.266149 0.148240 ... 0.493680 -0.216592 -0.543266 0.305322 -0.784593 0.532762 -0.148738 0.742088 -0.372970 0.710443
hiphop 0.171094 0.449266 0.447543 0.283238 -0.100621 -0.186977 0.449276 0.129152 -0.219113 0.465422 ... 0.408264 0.260917 -0.543491 -0.585650 0.695698 -0.667020 1.422759 -0.765019 1.277316 -0.152469
jazz -0.560886 0.319686 -0.769580 0.170312 -0.766630 0.526863 -0.684459 -0.006439 0.159303 -0.851891 ... -0.712067 -0.385368 0.634218 0.095933 0.260028 -0.483867 -0.147716 -0.247177 -0.249819 0.062708
metal -0.073127 0.353569 0.502345 -0.902857 0.065198 -0.244557 0.266441 1.010133 -0.346046 1.122383 ... 0.711583 0.318826 -0.433773 -0.932222 1.311821 -1.029750 1.069734 -0.735267 1.189815 -0.881726
pop 1.455405 -0.034044 1.288320 -0.362690 1.224541 -1.145759 1.315076 -0.403529 -0.474510 0.880912 ... 0.783727 -0.151191 -1.110466 0.967487 -1.477903 0.636546 -1.024685 0.956033 -0.852722 0.912750
reggae 0.423297 -0.033274 -0.009959 -0.757139 -0.396511 -0.484485 0.153553 0.137896 -0.393277 -0.536040 ... -0.476479 0.378747 -0.205455 0.606452 -0.312523 0.886485 -0.179706 0.637009 0.191636 0.335474
rock 0.221846 0.426527 0.231215 0.095492 -0.106560 0.231259 0.240894 0.064162 -0.233779 0.282125 ... 0.323387 -0.569342 -0.136208 -0.326718 0.249476 -0.186882 0.175498 -0.347977 0.190707 -0.714166

10 rows × 30 columns

Nu de data volledig is gegroepeerd op cluster kan de data van beide datasets worden vergeleken met elkaar. Op deze wijze zullen we bepalen welke genres aanwezig zijn in de unlabeled dataset.

Afstanden ¶

Om de eerste blik op de verschillende genres te werpen, berekenen we voor elke feature de kortste afstand tussen de clusters en de genres. Hierbij maken we gebruik van de volgende afstanden:

In [ ]:
# Maken functie minkowski_afstand
def minkowski_afstand(x1, x2, p):
    """
    Deze functie berekent de afstand tussen twee punten door middel
    van de minkowski formule.

    Parameters:
    ----------
    x1 : numpy array of list
        Een set aan verschillende data punten

    x2 : numpy array of list
        Een set aan verschillende data punten

    p : int
        Parameter nodig voor de minkowski formule

    Returns:
    ----------
    minkowski : int or float
        De afstand tussen de punten volgens
        de minkowski formule
    """
    # Omzetten van eventuele losse varaibelen in een lijst
    if not isinstance(x1, (list, np.ndarray)):
        x1 = [x1]
    if not isinstance(x2, (list, np.ndarray)):
        x2 = [x2]

    # Berekenen van de afstand tussen de punten
    afstand = [abs(a - b) ** p for a, b in zip(x1, x2)]
    minkowski = sum(afstand) ** (1/p)
    return minkowski

# Maken functie manhattan_afstand
def manhattan_afstand(x1, x2):
    """
    Deze functie berekent de afstand tussen twee punten door middel
    van de manhattan formule.

    Parameters:
    ----------
    x1 : numpy array of list
        Een set aan verschillende data punten

    x2 : numpy array of list
        Een set aan verschillende data punten

    Returns:
    ----------
    manhattan : int or float
        De afstand tussen de punten volgens
        de manhattan formule
    """
    # Omzetten van eventuele losse varaibelen in een lijst
    if not isinstance(x1, (list, np.ndarray)):
        x1 = [x1]
    if not isinstance(x2, (list, np.ndarray)):
        x2 = [x2]

    # Berekenen van de afstand tussen de punten
    afstand = [abs(a - b) for a, b in zip(x1, x2)]
    manhattan = sum(afstand)

    return manhattan

# Maken functie euclidische_afstand
def euclidische_afstand(x1, x2):
    """
    Deze functie berekent de afstand tussen twee punten door middel
    van de euclidische formule.

    Parameters:
    ----------
    x1 : numpy array of list
        Een set aan verschillende data punten

    x2 : numpy array of list
        Een set aan verschillende data punten

    Returns:
    ----------
    euclidisch : int or float
        De afstand tussen de punten volgens
        de euclidische formule
    """
    # Omzetten van eventuele losse varaibelen in een lijst
    if not isinstance(x1, (list, np.ndarray)):
        x1 = [x1]
    if not isinstance(x2, (list, np.ndarray)):
        x2 = [x2]

    # Berekenen van de afstand tussen de punten
    afstand = [abs(a - b) for a, b in zip(x1, x2)]
    euclidisch = np.sqrt(sum(d ** 2 for d in afstand))

    return euclidisch

Nu de afstand functies zijn gemaakt is het mogelijk om de genres te bepalen door middel van de afstanden. Dit gaat als volgt in werking:

  • Stap een is om per feature een leeg dataframe aan te maken waarbij de kolommen de genres zijn en de index gelijk is aan de clusters.
  • Stap twee is om voor de huidige feature de afstand te berekenen voor elk cluster naar elk genre. Dit levert dus 10 afstanden op per cluster.
  • Stap drie is om de kleinste afstand te selecteren tussen deze 10 afstanden, en het corresponderende genre op te slaan.
  • Stap vier is om te kijken welk genre het meeste voorkomt tussen de afstanden. Ook wordt het tweede genre bekeken.
  • Stap vijf is om de resultaten te tonen.

Omdat dit drie keer moet gebeuren is er een functie gemaakt om dit uit te voeren.

In [ ]:
from collections import Counter

def bepaal_genres(data, afstand_func, *args):
    """
    Deze functie neemt een afstandsfunctie en bepaald de afstand van
    genre naar cluster. Dit gebeurt voor alle clusters en alle genres,
    het uiteindelijke antwoord is de meest voorkomende genre voor elk
    cluster.

    Parameters:
    ----------
    afstand_functie : func
        De afstands functie waarmee de afstand word berekend.

    Returns:
    ----------
    De meest voorkomende genre (de meeste kleine afstanden) per cluster
    """
    # Aanmaken dictionaries
    afstand_dfs = {}
    afstand = {}
    antwoorden = {}

    # Stap 1
    # For loop om dataframes te maken met afstanden
    for col in data.columns:
        # Aanmaken dataframe voor kolom met genres en clusters als index en kolom
        df_afstand = pd.DataFrame(index=data.index, columns=scdfl.index)
        # Stap 2
        for x, row1 in data.iterrows():
            for y, row2 in scdfl.iterrows():
                if args:
                    # Voor afstands functies met extra argumenten
                    df_afstand.at[x, y] = afstand_func(row1[col], row2[col], *args)
                else:
                    # Voor afstands functies met alleen a en b
                    df_afstand.at[x, y] = afstand_func(row1[col], row2[col])
        # Vullen van dict met feature afstanden
        afstand_dfs[col] = df_afstand

    # Stap 3
    # For loop voor kiezen juiste afstand
    for _, df_afstand in afstand_dfs.items():
        for i, row in df_afstand.iterrows():
            # Vinden van genre met kleinste afstand
            min_genre = min(row.items(), key=lambda x: x[1])[0]
            # Aanmaken key voor values als deze nog niet bestaat
            if i not in afstand:
                afstand[i] = []
            # Toevoegen kleinste genre afstand
            afstand[i].append(min_genre)

    # Stap 4
    # For loop om meest voorkomende en op één na meest voorkomende genre te vinden
    for key, value in afstand.items():
        # Vinden van meest voorkomende en op één na meest voorkomende genres en tellen
        genre_1, aantal_1 = Counter(value).most_common(1)[0]
        genre_2, aantal_2 = Counter(value).most_common(2)[1]

        # Maken resultaten dict
        antwoorden[key] = {
            'Genre 1': genre_1,
            'Aantal 1': aantal_1,
            'Genre 2': genre_2,
            'Aantal 2': aantal_2
        }

    # Stap 5
    # Printen van de uitkomsten per cluster
    print(f"\nGenres volgens {afstand_func.__name__}:")
    for key, value in antwoorden.items():
        print(f"Cluster {key}: {value}")

    return afstand_dfs


# Kijken welke clusters tot welke genre horen
df_euclidisch = bepaal_genres(scdf_grouped, euclidische_afstand)
df_manhattan = bepaal_genres(scdf_grouped, manhattan_afstand)
df_minkowski = bepaal_genres(scdf_grouped, minkowski_afstand, 2)
Genres volgens euclidische_afstand:
Cluster 0: {'Genre 1': 'classical', 'Aantal 1': 6, 'Genre 2': 'reggae', 'Aantal 2': 4}
Cluster 1: {'Genre 1': 'pop', 'Aantal 1': 10, 'Genre 2': 'disco', 'Aantal 2': 7}
Cluster 2: {'Genre 1': 'metal', 'Aantal 1': 12, 'Genre 2': 'hiphop', 'Aantal 2': 9}

Genres volgens manhattan_afstand:
Cluster 0: {'Genre 1': 'classical', 'Aantal 1': 6, 'Genre 2': 'reggae', 'Aantal 2': 4}
Cluster 1: {'Genre 1': 'pop', 'Aantal 1': 10, 'Genre 2': 'disco', 'Aantal 2': 7}
Cluster 2: {'Genre 1': 'metal', 'Aantal 1': 12, 'Genre 2': 'hiphop', 'Aantal 2': 9}

Genres volgens minkowski_afstand:
Cluster 0: {'Genre 1': 'classical', 'Aantal 1': 6, 'Genre 2': 'reggae', 'Aantal 2': 4}
Cluster 1: {'Genre 1': 'pop', 'Aantal 1': 10, 'Genre 2': 'disco', 'Aantal 2': 7}
Cluster 2: {'Genre 1': 'metal', 'Aantal 1': 12, 'Genre 2': 'hiphop', 'Aantal 2': 9}

Genres volgens minkowski_afstand:
Cluster 0: {'Genre 1': 'classical', 'Aantal 1': 6, 'Genre 2': 'reggae', 'Aantal 2': 4}
Cluster 1: {'Genre 1': 'pop', 'Aantal 1': 10, 'Genre 2': 'disco', 'Aantal 2': 7}
Cluster 2: {'Genre 1': 'metal', 'Aantal 1': 12, 'Genre 2': 'hiphop', 'Aantal 2': 9}

Zoals er te zien is in de resultaten, zijn voor elke afstand dezelfde antwoorden gevonden. Deze resultaten leiden tot de volgende conclusies:

  • Cluster 0 is waarschijnlijk klassieke muziek
  • Cluster 1 is waarschijnlijk pop
  • Cluster 2 is waarschijnlijk metal

Terug naar boven

Grafieken ¶

Om een beter begrip te krijgen over welke clusters bij welke genres horen gaan we gebruik maken van scatterplots. In deze scatterplots zou het te zien moeten zijn welke factoren overlappen het meest met elkaar overlappen. Dit moet voornamelijk gebeuren voor clusters 0 en 1, cluster 2 is er waarschijnlijk zo uit te halen. Om dit toch met zekerheid te kunnen bevestigen zullen we beginnen met cluster 2. Omdat dit proces meerdere malen herhaald zal moeten worden maken we gebruik van een functie.

In [ ]:
def vergelijking_genres(data1, data2, x, y):
    """
    Deze functie maakt twee scatterplots met de datapunten
    van aangegeven clusters en/of genres.

    Parameters:
    ----------
    data1 : pandas.DataFrame
        De data uit het geclusterde dataframe

    data2 : pandas.DataFrame
        De data uit het gelabelde dataframe

    x : str
        De kolomnaam van feature 1

    y : str
        De kolomnaam van feature 2
    """
    # Maken van de plotsize
    plt.figure(figsize=(15, 6))

    # Maken eerste scatterplot
    sns.scatterplot(data=data1,
                    x=x,
                    y=y,
                    hue='cluster',
                    palette='viridis')
    
    # Maken tweede scatterplot
    sns.scatterplot(data=data2,
                    x=x,
                    y=y,
                    hue='genre')
    plt.show()

De eerste vergelijkingen van features die we gaan bekijken is tussen de tempo en de bandbreedte.

In [ ]:
# Invoeren welke clusters er getoond moeten worden
clusters = [1]

# Invoeren welke genres er getoond moeten worden
genres = ['pop', 'disco']

# Invoeren van databronnen en kolommen voor de assen
data1 = scdf[scdf['cluster'].isin(clusters)]
data2 = sc_label[sc_label['genre'].isin(genres)]
x = 'mean_tempo'
y = 'mean_bandwidth'

# Uitvoeren van vergelijking
vergelijking_genres(
    data1=data1,
    data2=data2,
    x=x,
    y=y
)

Zoals er te zien is overlappen de genres van pop en disco niet goed. De cluster zit voornamelijk in het gedeelte van pop, dus is het goed aan te nemen dat pop het algemene genre is van cluster 1.

Nu zal er gekeken worden naar cluster 0. Hiervoor zullen dezelfde assen worden gebruikt, om deze reden worden x en y niet opnieuw gedefineerd.

In [ ]:
# Invoeren welke clusters er getoond moeten worden
clusters = [0]

# Invoeren welke genres er getoond moeten worden
genres = ['classical', 'jazz']

# Invoeren van databronnen en kolommen voor de assen
data1 = scdf[scdf['cluster'].isin(clusters)]
data2 = sc_label[sc_label['genre'].isin(genres)]

# Uitvoeren van vergelijking
vergelijking_genres(
    data1=data1,
    data2=data2,
    x=x,
    y=y
)

Zoals er te zien is, is het genre jazz uitgespreid over de gehele grafiek. Het genre classical is echter gefocussed rond het midden van cluster 0. Om deze reden is het aan te nemen dat classical waarschijnlijker is dan jazz.

Nu is het op naar cluster 2. Deze heeft het genre metal of hiphop.

In [ ]:
# Invoeren welke clusters er getoond moeten worden
clusters = [2]

# Invoeren welke genres er getoond moeten worden
genres = ['hiphop', 'metal']

# Invoeren van databronnen en kolommen voor de assen
data1 = scdf[scdf['cluster'].isin(clusters)]
data2 = sc_label[sc_label['genre'].isin(genres)]

# Uitvoeren van vergelijking
vergelijking_genres(
    data1=data1,
    data2=data2,
    x=x,
    y=y
)

Bij deze grafiek zie je niet heel veel. Het enige dat opvalt is dat metal een uitschieter buiten het cluster heeft. Om verder onderzoek te doen nemen we een kijkje bij andere features, totdat we een duidelijke conclusie kunnen trekken.

In [ ]:
# Invoeren van nieuwe x en y
x = 'mean_rms_energy'
y = 'mean_tonnetz'

# Uitvoeren van vergelijking
vergelijking_genres(
    data1=data1,
    data2=data2,
    x=x,
    y=y
)

Bij deze grafiek zit hiphop iets meer naar de buitenkant van het cluster toe, terwijl metal wat meer in de kern van het cluster is.

In [ ]:
# Invoeren van nieuwe x en y
x = 'mfcc4_mean'
y = 'mean_bandwidth'

# Uitvoeren van vergelijking
vergelijking_genres(
    data1=data1,
    data2=data2,
    x=x,
    y=y
)

Bij de verdeling over de bandwidth en de mfcc_4 is de verdeling ook meer geneigd naar metal toe. Hiermee leid ons relatief korte onderzoek naar het genre metal toe. Wij zullen dit nu gaan uploaden naar Kaggle. Hiervoor moet er een csv bestand aangemaakt worden die alleen het genre en de filename bevat. Dit zal gebeuren met behulp van een functie.

In [ ]:
def kaggle_upload(data, cluster_map, naam_csv):
    """
    Deze functie maakt een dataframe aan en
    zet deze om naar een csv. Dit CSV bestand
    kan vervolgens geupload worden naar Kaggle.

    Parameters:
    ----------
    cluster_map : dict
        Een dict met vaste keys 0, 1 en 2. De values
        van de dictionary zijn de genres waarin de
        clusters zich lijken te bevinden.

    naam_csv : str
        De naam van het csv bestand.

    Returns:
    ----------
    csv
        Een csv bestand dat klaar is om op
        kaggle geupload te worden.
    """
    # Copy maken van scdf
    kaggle = data.copy()

    # Vervangen van de cluster nummers naar genres
    kaggle['cluster'] = kaggle['cluster'].replace(cluster_map)
    kaggle_df = pd.DataFrame(kaggle['cluster'].reset_index())
    kaggle_df.rename(columns={'cluster': 'genre'}, inplace=True)
    kaggle_df.set_index('filename', inplace=True)

    kaggle_df.to_csv(f'{naam_csv}.csv')

Met de functie ready to go, kan de eerste csv worden aangemaakt.

In [ ]:
# Maken van dictionary voor vervangen cluster nummers
cluster_map = {
    0 : 'classical',
    1 : 'pop',
    2 : 'metal'
}

# Aanmaken csv voor Kaggle
#kaggle_upload(scdf, cluster_map, 'submission_cl_me_po')

De submission op kaggle had een score van 0.98113. Dit is al drie of vier maal zo geweest onder gebruik van kleine aanpassingen in het algoritme.

Terug naar boven

PCA ¶

Principal Component Analysis (PCA) is een methode waarmee je de complexiteit van grote datasets kunt verminderen door ze om te zetten naar een kleiner aantal variabelen, terwijl je toch de belangrijkste informatie uit de oorspronkelijke dataset behoudt. (Jaadi, 2023), (Kumar, 2021), (StatQuest with Josh Starmer, 2018), (Krish Naik, 2018)

Voorbeeld:

Dit voorbeeld is opgesteld door de stappen te volgen uit het artikel geschreven door Nishant Kumar (2020). Hierbij zullen we gebruik maken van de volgende denkbeeldige dataset:

Voorbeeld iteratief process:

Dit voorbeeld is opgesteld door de stappen te volgen uit het artikel geschreven door Sharma (2023). Hierbij zullen we gebruik maken van de volgende denkbeeldige dataset:

X1 X2
1 2.5 3.5
2 4.2 5.0
3 1.8 1.5
4 3.5 4.0
5 2.8 2.7

Stap 1: Het standaardiseren van de dataset

De formule voor het standaardiseren is x - gemiddelde / standaarddeviatie. Berekenen van:

  • Het gemiddelde van X1 = (2.5 + 4.2 + 1.8 + 3.5 + 2.8) / 5 = 2.96
  • Het gemiddelde van X2 = (3.5 + 5.0 + 1.5 + 4.0 + 2.7) / 5 = 3.34
  • De standaardiviatie van X1 = $ \sqrt{\frac{(2.5 - 2.96)^2 + (4.2 - 2.96)^2 + (1.8 - 2.96)^2 + (3.5 - 2.96)^2 + (2.8- 2.96)^2}{5}} = 0.8260750571225354 $
  • De standaardiviatie van X2 = $ \sqrt{\frac{(3.5 - 3.34)^2 + (5.0 - 3.34)^2 + (1.5 - 3.34)^2 + (4.0 - 3.34)^2 + (2.7- 3.34)^2}{5}} = 1.1842297074469967 $
  • De standaardisatie van:
    • X1(1) = (2.5 - 2.96) / 0.8260750571225354 = -0.5568501264307829
    • X1(2) = (4.2 - 2.96) / 0.8260750571225354 = 1.5010742538568933
    • ... en zo door dan krijg je het volgende dataframe:
X1 X2
1 -0.55685013 0.13510892
2 1.5010742538568933 1.40175507
3 -1.40423075 -1.55375261
4 0.65369363 0.55732431
5 -0.193687 -0.54043569

Stap 2: De covariantiematrix berekenen

C= 1/n−1 Z^T Z

Daar komt uit:

array(1.25 , 1.16993124, 1.16993124, 1.25 )

Stap 3: Berekenen van eigenvals en eigenvecs

Eigenvals = [0.08006876 2.41993124]

Eigenvecs = [[-0.70710678 -0.70710678] [ 0.70710678 -0.70710678]]

Stap 4: Bereken van PCA

Principal Component 1 (first value)=Standardized X1 Eigenvector 1, Component 1 + Standardized X2 Eigenvector 1, Component 2

Principal Component 1 (first value) = -0.55685013 -0.70710678 + 0.13510892 -0.70710678 = 0.29821606899640385

Principal Component 1 (second value) = 1.5010742538568933 -0.70710678 + 1.40175507 -0.70710678 = -2.052610296082025

En zo verder

Het uitvoeren van een PCA begint bij het standadiseren van de (numerieke)data, zodat elke feature een gemiddelde van 0 en een variantie van 1 heeft. Het is belangrijk om de data te standadiseren omdat de covariantiematrix niet schaalinvariant is en veranderingen in de schaal van verschillende features zullen resulteren in onjuiste ontledingen. In het kopje clustering is een functie aangemaakt die de data scaled. Hieronder laten we het gestandadiseerde dataframe zien.

In [ ]:
display(scdf)
mean_bandwidth mean_beat mean_centroids mean_harmonie mean_rms_energy mean_spectral_contrast mean_spectral_rolloff mean_tempo mean_tonnetz mean_zcr ... mfcc20_mean mfcc2_mean mfcc3_mean mfcc4_mean mfcc5_mean mfcc6_mean mfcc7_mean mfcc8_mean mfcc9_mean cluster
filename
m00003.wav -0.282118 0.101179 -0.118884 -1.781320 -0.005254 0.078336 -0.205532 0.387660 -0.332126 0.027061 ... -0.564025 -0.044274 -0.953633 2.103414 -0.223317 0.781341 -0.632439 0.962736 -1.800265 2
m00012.wav 0.019216 -0.440457 0.616770 0.310657 0.947308 -0.083214 0.293430 -0.702430 -0.452162 1.407482 ... 2.264119 -0.669653 -0.886789 1.719003 -1.585550 1.483047 -1.130237 1.147159 -0.935620 2
m00013.wav -0.569065 0.156956 -0.458058 -0.511490 -1.262254 1.447517 -0.498065 2.009818 -0.085519 0.106458 ... 0.345629 0.067205 -0.995640 0.621481 -1.067151 0.774544 -1.143036 0.989746 -0.158202 0
m00043.wav -0.278768 0.162864 0.027796 -0.046399 -0.425318 -0.236340 -0.106240 0.387660 0.005644 0.168636 ... -1.208863 -0.197937 -1.140219 2.280413 -0.269944 1.218515 -0.539675 1.649282 -1.265518 2
m00044.wav -1.920661 -1.187562 -1.766122 0.281819 -1.382571 0.018429 -1.774679 0.639996 -1.382611 -1.593567 ... -1.297411 1.823784 0.808667 -1.562392 0.138805 -1.570094 -0.993139 -1.095278 -0.188952 0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
m00971.wav -0.957653 -0.164036 -0.938974 0.294107 -1.147540 -0.551006 -1.008327 -0.558598 0.725921 -0.653397 ... 0.042968 1.277592 -1.274369 -0.838470 -0.847983 -0.982974 -0.438186 -1.385951 -0.626994 0
m00973.wav 1.431593 0.172606 1.475285 0.277186 1.444242 -1.196850 1.498087 0.160558 -1.137006 1.021128 ... 0.745321 -1.137606 1.381635 -0.765037 0.936434 -0.155219 1.402402 0.072683 0.612714 1
m00988.wav 1.523881 -0.067850 1.778059 0.288494 1.222169 -1.318630 1.683362 -0.702430 -0.606247 1.152154 ... -1.140236 -1.588808 1.001067 -1.726364 0.406427 -0.215518 0.647399 -0.092322 1.506906 1
m00991.wav -1.215174 0.871910 -1.548642 0.290478 -1.402743 1.448930 -1.437098 -0.958130 -0.168995 -1.781361 ... 0.033323 1.625074 0.395099 0.187274 0.774719 0.146901 -0.268150 0.379999 0.147579 0
m00995.wav -1.261066 0.140836 -1.245551 0.290723 -0.821582 1.493382 -1.222552 0.922018 -0.078143 -1.128302 ... -2.117523 1.627783 -0.955432 -0.219515 -0.849474 -0.385089 -1.030321 -0.403109 -1.222124 0

105 rows × 31 columns

De tweede stap van het uitvoeren van een PCA is het maken van een covariantiematrix. Deze meet de correlatie tussen de features. Als de matrix positief is, betekent dit dat de variabelen samen stijgen of dalen. Als de matrix negatief is, betekennt dit dat waneer de ene variabele stijgt de andere daalt.

In [ ]:
cov_mat = scdf.cov()

Na het maken van de covariantiematrix worden de eigenvals en eigenvecs berekend. Eigenvectors is de richting waar de data naar toe is verspreid en eigenvalues is het relatieve belang van deze richtingen.

In [ ]:
eigenvals, eigenvecs = np.linalg.eig(cov_mat) 
print(f"The magnitudes of the projections are: {eigenvals}")
print(f"The vectors of the projections are: {eigenvecs}")
The magnitudes of the projections are: [8.86043756e+00 9.35376338e+00 2.34731855e+00 1.36317130e+00
 1.28129027e+00 1.06001977e+00 9.48542147e-01 8.15875599e-01
 7.68595900e-01 7.50606802e-01 5.70388088e-01 4.07530980e-01
 3.40763722e-01 2.89878119e-01 2.55401152e-01 2.26614198e-01
 2.03728912e-01 1.97284116e-01 1.70387030e-01 1.41963867e-01
 1.37662625e-01 9.57582779e-02 8.09942877e-02 1.17186755e-03
 4.54723431e-03 8.77722114e-03 6.56831981e-02 2.89981881e-02
 3.39860578e-02 4.45701453e-02 4.22743632e-02]
The vectors of the projections are: [[-3.20807365e-01 -7.49246134e-02 -2.49008038e-02 -8.13300583e-02
   1.70957390e-02  1.97794408e-02  1.63928921e-02  2.87093492e-02
  -2.34449831e-02 -5.63540324e-02 -4.46981281e-02 -5.81123661e-02
  -3.11770861e-03 -1.03163366e-01 -2.98902536e-02  1.47447075e-01
   3.08429816e-02  8.63072200e-02  1.05157004e-01  1.50135528e-01
   2.53779309e-02 -1.37958234e-01  2.37649336e-01  1.82041505e-01
   7.58140699e-01 -1.75614564e-01 -2.46345918e-02 -5.21277128e-02
   1.59053445e-01 -9.07400646e-02 -2.15187220e-01]
 [ 1.82646938e-02  8.96341470e-03  1.30523254e-01 -1.08907040e-01
   3.14971068e-01  7.04202764e-01  4.60423184e-01 -6.10918268e-02
  -6.10607114e-02  3.04428182e-01 -1.22860041e-01  5.58130950e-02
   7.92530594e-02 -1.20276864e-01 -5.34011736e-02 -2.92908589e-02
  -5.86040316e-02 -9.21274786e-02 -2.10853606e-03 -5.40090216e-02
  -5.15386146e-02  6.83890648e-03  3.42648189e-03 -5.23606262e-03
   6.13095293e-03  3.49399633e-02  3.66164465e-02  2.16480619e-03
   2.14696563e-02 -9.57324549e-03  6.72481589e-03]
 [-2.96093161e-01 -1.37721755e-01 -3.28823400e-03 -8.77270766e-02
   6.70050599e-02  3.06530609e-02 -1.54539660e-02 -4.89898120e-02
   1.97785180e-02 -1.56525724e-01 -9.24691076e-02 -3.28785621e-02
   1.25219922e-01  1.54322130e-02 -1.02961187e-01  4.68150639e-02
  -2.19884289e-02  3.53505858e-02 -3.15605730e-02  2.92698675e-02
   2.60858331e-03 -1.42952448e-02 -2.19723553e-02  7.49582885e-01
  -4.34076540e-01 -1.91360327e-01  9.72912466e-04  1.30429096e-02
   1.51016632e-01  1.15216547e-02 -6.87583150e-03]
 [-5.24164049e-02  8.83067851e-02  5.78650557e-02 -2.57603289e-01
  -4.99696830e-01  1.68259737e-01  1.53457767e-01  1.09520730e-01
   7.33707720e-01  1.42993804e-02 -3.83166552e-02  8.99557379e-02
  -7.21350060e-02  1.69071419e-01 -2.82079529e-02  7.28971428e-02
   4.18767383e-03  6.51918656e-02 -4.74984679e-02 -5.56083647e-02
   6.11099987e-02  3.35995004e-02  1.56387477e-02 -2.47555257e-03
   5.44742831e-03  2.63240206e-02  2.80074666e-02  2.44481329e-02
   3.43540448e-02 -1.37445027e-02 -6.78232798e-04]
 [-2.56802999e-01 -1.22805812e-01 -8.87819914e-02  4.40615226e-02
   8.64240387e-03  7.94261139e-02  1.02212681e-01  1.39725267e-01
  -1.39566163e-02 -1.18193809e-01  4.50656176e-01  1.47507061e-01
  -3.62880071e-01 -1.58841685e-01  3.70514121e-02 -6.12177642e-02
   1.68750917e-01 -7.10370153e-02 -7.47909911e-02 -3.09022949e-01
  -2.31149077e-01  1.28067322e-02 -2.28993008e-01  8.06233969e-03
   1.02683114e-01 -1.44659161e-01 -1.24482808e-01  1.61040440e-01
   9.83699278e-02  2.88914411e-01  2.54930575e-01]
 [ 2.83801176e-01  3.53746116e-02  1.24398040e-01 -2.60263868e-02
   7.38945474e-03 -1.15321010e-01  1.52118280e-01  1.77278369e-01
  -2.07657459e-02 -7.33970032e-02  4.42856409e-02  3.46908899e-02
   7.24873063e-02  2.75743283e-03 -7.20916334e-01 -5.80938092e-02
   2.81782848e-01  2.08327437e-01  8.87132069e-02  1.22273661e-02
  -2.77296890e-01 -2.59858192e-01  4.81517542e-02  2.94125652e-02
   9.33974670e-03 -5.98064876e-02  5.56517069e-02  1.12667958e-02
  -1.01929706e-01 -3.79347144e-03  3.27833703e-02]
 [-3.08722123e-01 -1.14011142e-01 -1.54731486e-02 -8.98760595e-02
   3.10418019e-02  3.24438912e-02  1.13825557e-02 -6.40102488e-03
   3.14477463e-03 -1.07999096e-01 -6.01655385e-02 -3.25423151e-02
   8.96625414e-02 -7.20122916e-02 -7.55842820e-02  9.32929927e-02
  -1.74390577e-03  1.03668125e-01  4.71624853e-02  9.15880221e-02
   5.76447340e-03 -7.65256698e-02  1.07363992e-01 -5.94664993e-01
  -3.37994412e-01 -5.23024078e-01 -1.20555659e-02 -5.92576864e-02
   2.07345219e-01 -4.24810582e-03 -1.01621718e-01]
 [ 1.04537105e-01 -2.52562983e-02 -1.43309849e-02 -2.07006930e-01
  -1.90562368e-01  4.37288830e-01 -5.74439923e-01  5.27684572e-01
  -2.25965786e-01 -5.54116073e-02 -1.31871236e-01 -3.68524388e-02
  -3.43419650e-02 -9.68133167e-02 -2.83212516e-02 -2.70130383e-03
  -3.17029084e-02 -5.03691930e-02 -9.27496284e-02  4.69804595e-02
  -3.16904974e-02 -3.67751496e-02 -5.39578331e-02  4.19051765e-03
  -7.97754149e-03 -1.55381965e-02  2.13823124e-02  5.95174329e-02
  -2.91281684e-02  4.03955645e-03 -1.90142440e-02]
 [ 1.13594104e-01  9.77380254e-02 -1.29292796e-02  2.23581055e-01
   4.03322448e-01 -2.38295715e-02  2.49813909e-01  5.44541385e-01
   2.49581351e-01 -4.29280882e-01 -1.19286208e-01 -2.12604621e-01
  -1.41459779e-03  2.73579223e-02  1.50085991e-01  9.23407137e-02
  -7.60370195e-02 -8.88405929e-04  6.21878409e-02  8.09802015e-02
   9.97468945e-02  8.24641259e-02 -2.94699987e-02 -4.33571931e-03
  -1.96478857e-03  1.22180075e-03 -1.17836284e-01 -5.45064829e-03
  -5.96670412e-02  1.14215274e-01 -5.51597598e-02]
 [-2.12041738e-01 -1.96291784e-01  4.11248515e-02 -7.81368527e-02
   1.70171465e-01  2.18202750e-02 -1.17450428e-01 -1.26597951e-01
   8.52474434e-02 -2.80039173e-01 -1.84796597e-01  1.99013669e-02
   2.43663899e-01  2.01300589e-01 -2.52095529e-01 -1.27879756e-01
  -9.05657293e-02 -8.33094584e-02 -2.08278270e-01 -1.51496393e-01
   7.24685216e-02  1.98979745e-01 -3.88779326e-01 -1.27956437e-01
   2.57056760e-01 -1.97050846e-02  1.30200937e-01 -1.05046490e-01
  -1.99395515e-01 -1.84480073e-01  2.95525564e-01]
 [-1.37767209e-02 -2.88152729e-01  1.55931979e-01  7.05509151e-02
  -1.27372911e-01 -7.65948872e-02  7.91208298e-02  1.73979302e-01
  -6.28681996e-02 -2.95820244e-02 -2.66027401e-02  1.16486370e-02
   1.82755964e-01 -1.35944224e-01 -7.12259099e-03 -2.85174139e-01
  -1.29741710e-01  1.25471040e-01 -9.08431228e-02 -4.44430422e-01
  -1.93125953e-02  2.95427930e-01  4.90195946e-01 -8.35441387e-03
  -1.77870455e-02  7.48125125e-02 -2.60554628e-01  9.47479502e-02
   1.18479307e-02 -1.99271378e-01 -3.08553948e-02]
 [-1.81013198e-01  1.54827956e-01  5.97989960e-02  3.72384192e-01
   1.11125390e-01 -1.06846786e-02 -1.91938048e-01  1.25628353e-01
   7.85635826e-02  2.22130462e-01 -1.41340887e-01  5.48333952e-01
   4.23807257e-02  1.02027263e-01 -7.45064839e-02  3.19810559e-01
  -2.61938115e-01  8.66841211e-02  1.43842445e-01 -1.27051305e-01
  -6.58493297e-02 -1.75442700e-01 -1.50730725e-02  8.70022992e-03
  -1.19209859e-02 -2.04778808e-02 -2.37847481e-01  4.86629928e-02
  -7.88650963e-02 -1.03168281e-01  1.22440746e-01]
 [ 3.25059884e-02 -2.79703802e-01  4.72783807e-02  2.81392535e-01
  -1.21256715e-02 -3.04137709e-02 -5.53662140e-02  7.32263678e-02
   1.22882640e-01  1.04312731e-01  2.06226526e-01  4.35519944e-03
   1.17963746e-01 -1.25762236e-01 -2.35730703e-01  3.66298144e-01
  -1.62250196e-02 -3.50668220e-01 -5.99851804e-02 -1.61695911e-02
  -4.68418163e-02  2.74113644e-01 -9.17376453e-02 -3.26631938e-02
  -1.77440180e-02  1.06826188e-01  3.42966500e-01  1.44533333e-01
   1.63109860e-01 -3.16578274e-02 -3.87339885e-01]
 [-1.14528970e-01  2.25919758e-01  1.38129251e-01  3.32552174e-01
  -7.01243365e-02  1.12425738e-01 -1.49769141e-01 -3.81934327e-02
   1.72082355e-01 -1.65791387e-02  4.48463428e-02  8.41992272e-02
   3.78160608e-01 -3.24125989e-01  4.25597187e-02 -1.75562836e-01
   4.40142290e-01 -2.09727793e-01 -9.07040130e-02  1.45136731e-01
   1.91795555e-01 -7.28663999e-04  1.91682109e-01  2.50166226e-02
   1.79112472e-02 -6.63872649e-02  1.91442891e-02 -1.18198037e-01
  -9.36784730e-02  1.85554862e-01  2.03993044e-01]
 [ 3.05879320e-02 -2.45124696e-01  1.99011380e-01  3.35575180e-01
  -2.28504616e-02  1.42798083e-01 -1.11433075e-01 -1.29760260e-01
   1.89945519e-01 -2.88920866e-03 -5.42462084e-02 -1.45979420e-01
   3.78693748e-02 -2.50750990e-02  2.51733517e-01 -1.14474780e-01
   1.40484985e-02  3.89939616e-01 -2.47657786e-01  1.67680775e-01
  -5.16042938e-01 -1.43725685e-01 -1.82827975e-01 -8.99035604e-03
   1.98323022e-02  5.63314233e-02 -3.10293695e-02 -1.00146027e-01
   2.14766515e-02 -2.67876969e-02 -1.40758548e-01]
 [-7.75953663e-02  2.17906095e-01  2.84092035e-01  2.43082793e-01
  -1.06645668e-01  1.75700673e-01  1.55421862e-02 -1.02975226e-01
  -8.52466316e-02 -2.57462688e-01  2.74855193e-01 -1.33782099e-01
  -2.48840941e-01 -5.44000771e-02 -1.25181468e-01 -2.34642505e-02
  -3.59173166e-01  1.48429939e-01 -1.48723048e-02  2.06101934e-01
   1.44905601e-01  3.35978286e-03  1.59194675e-01 -1.60532341e-03
  -2.92559867e-02  1.50392502e-02  2.83581259e-01  2.29929700e-01
   3.41747461e-02 -2.51089418e-01  2.58529174e-01]
 [ 8.26728555e-02 -1.57396476e-01  4.47541086e-01  1.21463568e-01
  -7.96378047e-02  1.38683039e-01 -1.03026011e-01 -1.80758200e-01
  -7.55599178e-02 -6.51256878e-02 -1.75980220e-01 -2.53763123e-01
  -2.43130134e-01  1.55390947e-01 -1.03086528e-02  3.09139550e-01
   3.63844419e-01 -1.25107112e-02  3.32791910e-01 -1.97442784e-01
   1.73241375e-01  1.15679753e-01 -6.91732395e-02 -2.90189819e-03
  -7.00030248e-03 -2.35456412e-02 -2.45825459e-01 -3.86806145e-02
   3.37785963e-02 -1.61530147e-03  7.81027850e-02]
 [-4.00477399e-02  2.17446088e-01  3.60352675e-01 -1.05502170e-01
  -1.79455773e-01  1.01144874e-02  1.03309227e-01 -7.62951551e-02
  -1.77492175e-01 -2.73551770e-01  2.03785914e-01 -4.67395250e-04
   2.17589475e-01  3.56450770e-02  8.00470628e-02  1.20026256e-01
  -2.20530399e-01 -1.93043077e-01 -1.45324677e-01 -2.68597321e-01
  -2.39308790e-02 -3.09752817e-01 -9.27423295e-02  3.91964533e-03
   4.24831157e-03 -3.76004927e-02 -9.42696901e-02 -1.53700119e-01
  -2.50734076e-01  1.67908853e-01 -3.67060317e-01]
 [ 1.92158650e-02 -9.50987766e-02  4.78060631e-01 -2.28936644e-01
   2.11083867e-03 -2.73171396e-01  1.49891232e-01  1.06051860e-01
  -9.77023896e-02 -1.00177551e-01 -2.87326793e-01  4.40532488e-01
  -2.30375309e-01 -2.10032392e-01  1.68104737e-01 -1.07812639e-03
   6.87681779e-02 -1.62708369e-01 -1.27109738e-01  2.85175829e-01
  -1.19662573e-01  1.14928991e-01 -1.80868870e-02  1.08288490e-03
  -1.13804280e-02  6.59717481e-04  1.30161633e-01 -2.16497096e-03
   3.34302362e-02 -4.21558658e-02  5.59039362e-02]
 [-1.14330885e-01  1.88591045e-01  2.54539488e-01 -2.59588008e-01
   1.43802858e-01 -1.07172565e-01 -3.26727357e-03  2.21187526e-01
  -1.05718919e-01  2.41277931e-01  3.58129282e-01 -6.65541795e-02
   3.43215082e-01  1.22476109e-01  1.47480444e-01  2.33003344e-01
   1.95915151e-01  3.87395819e-01 -1.24632470e-01  5.40958932e-02
   6.60650070e-02  2.15670099e-01 -1.50806983e-01 -1.19059628e-02
   8.05054960e-03  7.37492854e-02 -1.32313071e-02  9.30355901e-02
   1.10504521e-01 -1.02652624e-01  9.69862688e-02]
 [-2.22108869e-01 -1.98646889e-01 -9.92553671e-02 -2.30955811e-02
   1.03209574e-01  1.49366274e-01  5.68339252e-04  8.47534464e-02
  -1.37623928e-02 -1.50061934e-01  2.95937320e-01  2.29139471e-01
  -1.97424576e-01  7.37413655e-02 -6.93220546e-03 -1.68332995e-02
   2.36690469e-01  6.35424128e-02  5.80209591e-02  9.96004056e-02
   1.46042863e-01 -8.21993241e-02  1.09005378e-01 -4.05371134e-02
  -1.73051527e-01  3.67377265e-01  2.20507012e-03 -4.47574003e-01
  -2.04971136e-01 -3.14612498e-01 -1.78736729e-01]
 [-8.02229307e-02 -5.87584805e-02  3.47335730e-01 -1.87495912e-01
   4.07749385e-01 -1.52561296e-01 -3.21899266e-01  4.05928172e-02
   3.47573261e-01  3.20681852e-01  1.57466692e-01 -2.10454854e-01
  -2.02213796e-01 -4.76188450e-02 -8.07782169e-02 -2.71760259e-01
  -1.62738736e-01 -1.54632625e-01  6.90988259e-02 -6.41400368e-02
   6.57772902e-02 -1.85144721e-01  8.93258346e-02  8.52054410e-03
  -2.15516148e-02 -4.46472442e-02  9.48690044e-03 -3.64199837e-02
  -7.45921503e-02  6.82480942e-02 -5.66479955e-02]
 [ 2.84192361e-01  1.51272691e-01 -3.38504646e-02  9.07371766e-02
  -2.30905954e-02  3.50721110e-04  9.26529564e-03  7.21734587e-02
   2.51950604e-02  1.29261101e-01  1.17840023e-01  3.68793812e-02
  -1.25597890e-01 -8.61588120e-02  1.27890572e-01 -4.51746695e-03
  -8.14549954e-03  4.07411805e-02  2.04096713e-02 -1.93293397e-01
  -3.84936633e-02  2.35631987e-01 -8.06483966e-02  1.64083362e-01
   4.08982661e-02 -5.79578542e-01  1.95105179e-01 -4.21251705e-01
  -1.36264817e-01 -3.26406348e-01 -2.88251677e-02]
 [-2.60672428e-01  1.11209751e-01 -7.68917512e-02 -5.95322722e-02
  -1.54036296e-01 -1.50662719e-01  9.62583094e-02  6.57010399e-02
   1.44775109e-03  1.42121723e-01 -1.52714212e-01 -3.66204486e-01
  -5.91419775e-02 -5.47205754e-01 -1.28700772e-01  2.65068535e-01
  -1.36142661e-01  2.38797629e-03  2.57447024e-02 -3.36463121e-02
  -1.60861126e-01  4.06572130e-02 -1.20154605e-01 -2.69928787e-02
  -7.82568709e-02  2.35690310e-01 -8.97118278e-02 -2.67973526e-01
  -1.80163834e-01 -7.20185668e-02  2.01553331e-01]
 [ 1.22924056e-01 -2.62998032e-01  2.42023320e-02  6.59482879e-02
  -1.46625244e-01 -3.21765937e-02  1.54473247e-01  1.50428068e-01
  -5.16686914e-02  1.61548087e-01  1.94205582e-01 -1.48706179e-01
   8.93173886e-02  2.66593377e-01 -1.27112897e-02  8.52851664e-02
  -1.13630749e-01 -4.02814509e-01 -2.12665739e-01  3.08634632e-01
  -3.05522853e-02 -1.87298160e-01  6.62505953e-02  2.06601369e-02
   1.88167980e-02 -1.17016800e-01 -3.35170773e-01 -1.75970011e-01
   1.15211522e-01 -1.70320231e-01  3.32509007e-01]
 [-2.17683299e-01  1.63744106e-01  1.30424452e-02  2.49559305e-01
  -9.27834371e-02 -8.09994913e-02  1.50310282e-01  1.51006050e-01
  -1.32417628e-01  2.45011209e-01 -2.15474287e-01 -1.01382213e-01
  -2.64093248e-01  7.87391154e-02 -1.60423873e-01 -2.22149169e-01
   1.69177280e-01  2.50705486e-02 -4.31023900e-01 -2.80164394e-02
   3.26576880e-01 -8.03831771e-02 -2.36668971e-01 -5.41366437e-05
  -2.01220041e-02 -5.83329086e-02 -1.02640302e-01  1.23559821e-01
   1.94419562e-02 -1.34634483e-01 -2.90562136e-01]
 [ 1.35662224e-02 -2.86723805e-01  5.95038297e-02  8.76090834e-02
  -1.72701149e-01 -8.14940785e-02  1.52293081e-01  2.14087643e-01
  -3.73848368e-02  7.40389975e-02 -2.03229019e-02  4.43221225e-03
   2.09480179e-01 -1.42011561e-01  2.10155265e-01 -1.77755940e-01
  -3.02992911e-02  6.42612694e-02  3.77532457e-01 -1.57086055e-01
   2.65351507e-01 -4.23638141e-01 -2.97331148e-01  6.45789307e-03
   1.28893247e-03  4.17750415e-02  3.08999519e-01  3.05966033e-02
   1.40401741e-01 -1.10351434e-01  1.14610957e-01]
 [-2.80720321e-01  1.06780533e-01  1.91565662e-02  9.65137055e-02
  -8.34684098e-03 -4.71387412e-02  4.55272072e-02  1.80425311e-01
  -1.23488889e-01  9.87909802e-02 -1.77726857e-01 -1.69930895e-01
  -5.20092100e-02  4.18394235e-01  1.37665438e-01  5.53386952e-02
   1.41615367e-01 -1.34193081e-01 -3.77829326e-02 -2.18940209e-01
  -3.09406151e-01 -7.05596633e-02  3.22768133e-01 -2.17603008e-02
  -2.95775622e-02  5.90623100e-02  4.89094661e-01 -9.94284579e-02
  -3.55418948e-02  9.65852663e-02  1.45277835e-01]
 [ 9.44775661e-03 -3.02221364e-01  6.01036207e-02  9.04975788e-02
  -1.10725179e-01  5.77999181e-03  6.85857773e-02  6.87141911e-02
  -1.08336477e-01  1.74317081e-01  1.80899887e-02  6.82376294e-02
  -6.07466829e-02  7.03688538e-02 -1.23452969e-01  6.23465688e-02
  -2.25957654e-01  3.16287872e-01 -8.36908409e-02  1.34088993e-01
   2.89161798e-01  1.60792926e-01  6.35020340e-02  3.07036762e-02
   4.71344584e-02 -6.36189807e-02  1.22817853e-01 -2.66749367e-01
  -2.15467426e-01  6.06383787e-01  5.49186993e-02]
 [-2.58146128e-01  1.38493758e-01  9.87174563e-02  7.82108526e-02
  -1.94738406e-01  2.15439427e-02  5.36005521e-02  1.14609356e-01
  -9.44838335e-02  7.60993823e-02  6.47541451e-02 -1.30247967e-02
   8.26561204e-02  1.87052953e-01 -7.84482413e-02 -3.87839396e-01
  -9.91064201e-02 -1.28599231e-01  5.03335143e-01  2.70887898e-01
  -2.34708579e-01  3.52142355e-01 -1.99903357e-01  3.82682105e-03
   5.02729670e-03 -1.04645239e-02 -1.34000599e-01 -4.38683119e-02
  -4.86142881e-02  5.16035844e-02 -1.71028318e-01]
 [-8.41142044e-02 -2.25165579e-01 -6.89364087e-02 -4.04152719e-02
  -3.40429422e-02  6.13444138e-03  8.22445406e-02 -6.54018854e-03
   2.19592416e-02  8.85838993e-02  3.42838707e-02 -7.04763527e-02
   2.29270263e-02 -1.09863876e-02  1.50744382e-01  1.12488672e-01
   1.22420323e-01 -2.02314672e-02  6.10429970e-02  1.46092951e-01
  -2.63681160e-02 -3.63334578e-02  5.37222004e-02  2.51300569e-02
  -3.19492777e-02 -2.10688052e-01  1.21485335e-02  4.77914112e-01
  -7.35051566e-01 -1.27759378e-01 -2.75538529e-02]]
In [ ]:
inds = eigenvals.argsort()
eigenvecs = eigenvecs[inds[::-1]]
eigenvals = eigenvals[inds[::-1]]
In [ ]:
plt.plot(eigenvals, '-o')
Out[ ]:
[<matplotlib.lines.Line2D at 0x184019d3750>]

Aan de hand van deze plot zien we dat de elleboog bij 3 ligt. Daarom kiezen wij voor 3 pca componenten.

Stap 4 is het uitvoeren van de PCA op de gestandaardiseerde data. We fitten de data en transformen deze.

In [ ]:
pca = PCA()
pca.fit(scdf)
transformed = pca.transform(scdf)
print(transformed.shape)
(105, 31)

Hier nog een plot om te laten zien dat de 3 eerste componenten het belangrijkste zijn.

In [ ]:
per_var = np.round(pca.explained_variance_ratio_*100, decimals=1)
labels = ['PC' + str(x) for x in range (1, len(per_var)+1)]

plt.bar(x=range(1, len(per_var)+1), height=per_var, tick_label = labels)
plt.ylabel('Percentage of Explained Variance')
plt.xlabel('Principal Component')
plt.title('Scree plot')
plt.show
Out[ ]:
<function matplotlib.pyplot.show(close=None, block=None)>
In [ ]:
xs = transformed[:,0]
ys = transformed[:,1]
plt.scatter(xs, ys)
plt.show()

Bij deze scatterplot kun je goed zien dat er waarschijnlijk 3 verschillende genres zijn. Ik zeg waarschijnlijk omdat je wel kunt zien dat er drie groepen zijn, maar of deze datapunten ook echt allemaal bij elkaar horen kunnen we straks in een andere scatterplot beter zien.

In [ ]:
unlabeled2 = unlabeled.copy()

#PCA componenten aanmaken
unlabeled2['PCA_Component_1'] = transformed[:, 0]
unlabeled2['PCA_Component_2'] = transformed[:, 1]
unlabeled2['PCA_Component_3'] = transformed[:, 2]

Het dataframe met de filenames en de pca componenten

In [ ]:
unlabeled2= unlabeled2[['PCA_Component_1', 'PCA_Component_2', 'PCA_Component_3']]
unlabeled2
Out[ ]:
PCA_Component_1 PCA_Component_2 PCA_Component_3
filename
m00003.wav 4.235612 -2.055890 -0.720285
m00012.wav 6.136067 -0.594984 0.646706
m00013.wav 1.177170 -2.997248 3.263309
m00043.wav 4.000373 -1.748447 -2.114490
m00044.wav -4.145249 -3.360042 -3.709625
... ... ... ...
m00971.wav -3.063247 -2.178624 -2.070839
m00973.wav -0.396747 4.693099 -0.285384
m00988.wav -0.097874 4.645286 -1.303385
m00991.wav -2.335465 -3.013813 2.475883
m00995.wav -1.469824 -4.060340 -0.940655

105 rows × 3 columns

In [ ]:
elleboog_KMeans(11, unlabeled2)

Aan de hand van deze grafiek kun je zien dat er 3 clusters zijn. Nu gebruiken we de functie clusteren die in het hoofdstuk clusteren is aangemaakt.

In [ ]:
df = clusteren(3, unlabeled2)
0    40
2    39
1    26
Name: cluster, dtype: int64

We groeperen de data per cluster op het gemiddelde

In [ ]:
# Groeperen van data per cluster
df_grouped = df.groupby('cluster').agg('mean')

# Tonen van het resultaat
display(df_grouped)
PCA_Component_1 PCA_Component_2 PCA_Component_3
cluster
0 -0.881684 3.475096 -0.164351
1 4.890509 -1.182574 -0.197208
2 -2.356049 -2.775819 0.300037
In [ ]:
sns.scatterplot(data=df,
               x= xs,
               y = ys,
                hue='cluster',
               palette= 'Set1')
plt.show()

Hier kun je duidelijk zien dat de drie groepen die we al eerder hadden gezien ook daadwerkelijk de drie verschillende genres zijn. De datapunten per cluster liggen allemaal dicht bij elkaar

In [ ]:
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')

# Scatter plot with 3D projection
scatter = ax.scatter(df['PCA_Component_1'], df['PCA_Component_2'], df['PCA_Component_3'], c=df['cluster'], cmap='viridis')

# Add labels and a color bar
ax.set_xlabel('PCA_Component_1')
ax.set_ylabel('PCA_Component_2')
ax.set_zlabel('PCA_Component_3')
ax.set_title('3D Scatter Plot with Clusters')
fig.colorbar(scatter, label='Cluster')

plt.show()

Ook hebben we een 3demensionale scatterplot gemaakt omdat we met 3 componenten werken. Ook hier zie je hetzelfde als bij de 2 demensionale scatterplot, dat de datapunten per cluster dicht bij elkaar liggen.

Om te kijken welke genre er bij de clusters horen, gaan we kijken naar de afstanden. We beginnen met de gelabelde data scalen.

In [ ]:
sc_label.drop(['Hz', 'data', 'genre'], axis=1, inplace=True)

Hier passen we de PCA op toe

In [ ]:
pca = PCA(n_components=3)
pca.fit(sc_label)
transformed_l = pca.transform(sc_label)
print(transformed_l.shape)
(50, 3)

Nu maken we features van de pca data en zetten deze in een dataframe van de gelabelde data.

In [ ]:
label = sc_label.copy()
label['genre'] = labeled['genre']

label['PCA_Component_1'] = transformed_l[:, 0]
label['PCA_Component_2'] = transformed_l[:, 1]
label['PCA_Component_3'] = transformed_l[:, 2]

# Print de resulterende DataFrame
print(label)
            mean_bandwidth  mean_beat  mean_centroids  mean_harmonie  \
filename                                                               
m00002.wav       -0.610133   0.359610       -1.092007       0.410972   
m00039.wav       -0.411385  -0.067294       -0.573296      -1.642859   
m00041.wav        1.535260  -0.231133        1.300127       0.343474   
m00072.wav        0.970634   0.300659        0.599644       0.322589   
m00096.wav        0.522519   0.285885        0.491595       3.504310   
m00102.wav       -1.414572   0.297681       -1.492100      -0.629824   
m00112.wav        0.740998   0.312225       -0.137938      -3.127108   
m00138.wav        0.141260   0.287360       -0.307725       0.328948   
m00192.wav       -0.519875  -5.888664       -0.476122      -0.271121   
m00206.wav        0.141239   0.496945        0.759211       0.300667   
m00230.wav        1.657057   0.424746        2.236136       0.325323   
m00236.wav       -1.619458   0.447912       -1.268198       0.308425   
m00248.wav        0.224815   0.178713        0.644430      -1.496280   
m00253.wav        0.267146   0.061034        0.337512       0.343280   
m00298.wav       -0.502545  -0.251752       -0.564401       0.322885   
m00313.wav       -1.523249   0.148585       -1.584653       0.317997   
m00338.wav       -0.595084   0.224581       -0.334379       0.321253   
m00339.wav       -0.245430   0.299309        0.377963       0.325102   
m00351.wav       -0.233972  -0.386570       -0.552227       0.345841   
m00400.wav       -2.460509  -0.520294       -2.362404       0.326717   
m00421.wav        1.998793   0.130544        2.188316       0.320502   
m00429.wav        0.204828   1.099529        0.091887       0.312079   
m00435.wav       -1.347296   0.034016       -0.843604      -0.399708   
m00454.wav        0.343704   0.057339        0.664594       0.307062   
m00477.wav       -1.515913  -1.872608       -1.495107       0.234502   
m00501.wav       -0.267606   1.247700       -0.696861      -0.434746   
m00503.wav       -0.375276   0.540452        0.366644      -1.300940   
m00513.wav        2.403656   0.198521        2.657701      -3.139432   
m00553.wav       -0.161993  -0.095679       -0.131943       0.317907   
m00606.wav       -0.529961  -1.866178       -1.065649       0.324885   
m00623.wav        1.498992  -0.614455        1.264272       0.323540   
m00627.wav       -0.264953  -0.083731        0.244040      -2.368593   
m00629.wav       -0.217338   0.286651       -0.472680       0.382821   
m00633.wav       -0.278625   0.129487       -0.474444       0.331835   
m00637.wav       -0.086584   0.293243        0.116784       0.220553   
m00658.wav        0.252283   0.299274        0.605239       0.275826   
m00671.wav        0.146621  -0.084203       -0.295108       0.331784   
m00676.wav        0.669594  -0.205831        0.519156       0.333844   
m00677.wav       -1.068777   0.080626       -1.129823       0.327368   
m00678.wav       -0.218363   0.943969        0.046656       0.319453   
m00716.wav       -0.280426   0.053313       -0.251540       0.369363   
m00762.wav        1.532382   0.147893        1.010290       0.333203   
m00772.wav       -0.596171   0.138500       -0.190153       0.325062   
m00773.wav        0.669723  -0.062322       -0.223699       0.328163   
m00801.wav        1.957072   0.371786        1.063803      -0.981344   
m00821.wav        0.268142   0.188443        1.209958       0.332074   
m00850.wav        0.617315   0.216768        0.570907       0.323577   
m00867.wav       -0.313402   1.116556       -0.292722       0.325932   
m00895.wav        0.307163   0.206484        0.197185       0.482706   
m00996.wav       -1.412294   0.324376       -1.255266       0.160130   

            mean_rms_energy  mean_spectral_contrast  mean_spectral_rolloff  \
filename                                                                     
m00002.wav        -1.148479                1.885706              -1.009321   
m00039.wav         0.143471                0.858330              -0.468786   
m00041.wav         1.462562               -1.542893               1.462870   
m00072.wav         0.484712               -0.456732               0.701757   
m00096.wav         1.216862                0.735994               0.687644   
m00102.wav        -1.872493                0.590103              -1.652879   
m00112.wav        -1.027873               -1.288618               0.153426   
m00138.wav        -0.655435                0.504976               0.000897   
m00192.wav        -0.007500               -0.731520              -0.616370   
m00206.wav        -0.212702               -0.472698               0.577007   
m00230.wav        -0.339340               -0.687443               1.928927   
m00236.wav        -1.664066                1.575199              -1.399621   
m00248.wav        -0.031329               -1.004730               0.514270   
m00253.wav         2.741119                0.687510               0.475302   
m00298.wav        -0.604069                0.380307              -0.408851   
m00313.wav        -0.465289                0.314094              -1.545533   
m00338.wav         1.432591                0.572837              -0.409993   
m00339.wav        -0.152732                0.886229               0.087414   
m00351.wav        -0.783332                0.961884              -0.364136   
m00400.wav        -0.876555                0.501466              -2.424284   
m00421.wav         2.406325               -1.032669               2.056261   
m00429.wav        -0.113470               -0.062799               0.273130   
m00435.wav        -0.583478                1.674181              -1.219162   
m00454.wav         0.091488                0.190610               0.648727   
m00477.wav        -1.749154                1.132896              -1.550718   
m00501.wav        -0.313032               -0.035933              -0.702820   
m00503.wav        -0.386350                0.277942              -0.005864   
m00513.wav         0.722845               -1.768020               2.556876   
m00553.wav         0.554915               -0.875933              -0.238857   
m00606.wav        -0.473707               -0.801763              -1.062261   
m00623.wav         0.483956               -1.179928               1.347176   
m00627.wav         0.283654               -0.417977               0.128453   
m00629.wav         0.696643                0.778216              -0.297322   
m00633.wav         0.317514                0.866001              -0.350447   
m00637.wav        -0.233907               -0.393514               0.175869   
m00658.wav        -0.034514               -0.196482               0.571649   
m00671.wav        -0.926675               -1.317186              -0.264949   
m00676.wav         2.100149               -2.045880               0.531674   
m00677.wav         1.069921                2.469404              -1.052861   
m00678.wav         0.414517                0.142417              -0.043891   
m00716.wav        -1.000883                0.490734              -0.142621   
m00762.wav         0.001160               -1.393168               1.199475   
m00772.wav        -0.599449                0.949102              -0.351265   
m00773.wav        -0.569178                0.660665              -0.032301   
m00801.wav        -0.972810               -0.717789               1.621881   
m00821.wav         0.045498               -0.220436               0.739237   
m00850.wav         0.523135               -0.813370               0.532147   
m00867.wav        -0.690522                1.121854              -0.273464   
m00895.wav         1.882714               -1.083102               0.119905   
m00996.wav        -0.587426               -0.668079              -1.203396   

            mean_tempo  mean_tonnetz  mean_zcr  ...  mfcc4_mean  mfcc5_mean  \
filename                                        ...                           
m00002.wav    0.556581     -0.201259 -1.393183  ...   -0.414782   -0.455867   
m00039.wav    0.862029      0.548655 -0.874438  ...    0.416474    0.080578   
m00041.wav   -0.948033     -1.866566  1.124088  ...   -2.010779    0.114446   
m00072.wav    0.556581     -1.074005  0.315799  ...   -1.929537    0.626036   
m00096.wav    0.280223      0.207270  0.207668  ...   -0.098669   -1.324775   
m00102.wav    0.280223      1.795694 -1.019308  ...   -0.854354    0.301705   
m00112.wav    1.580730     -2.073776 -1.154037  ...    0.028416    2.184078   
m00138.wav    0.280223      0.137251 -0.920355  ...   -0.285322   -0.192336   
m00192.wav    0.556581     -0.692819  0.101782  ...   -0.849080   -0.276803   
m00206.wav   -0.200399     -0.071087  1.240265  ...    0.989720   -0.946793   
m00230.wav    0.280223     -0.492165  2.596123  ...    0.073349    0.762104   
m00236.wav    0.280223      0.962270 -0.710080  ...   -0.992708    0.237479   
m00248.wav   -0.410671     -0.477346  1.269286  ...    1.808582   -0.835496   
m00253.wav   -1.729651     -0.427638  0.426625  ...    0.485522   -0.769230   
m00298.wav   -1.619736      0.704218 -0.700220  ...    0.155711   -1.492965   
m00313.wav   -1.101566     -1.621150 -1.380740  ...    1.637573    1.425775   
m00338.wav   -0.782691      0.321627 -0.021645  ...    1.542112   -0.486405   
m00339.wav    0.556581     -0.308531  1.075439  ...    0.580677   -1.169200   
m00351.wav   -0.782691      1.201644 -0.757026  ...    0.185749   -0.640877   
m00400.wav   -2.608971      1.101189 -2.123989  ...   -1.040294    2.247019   
m00421.wav   -1.244509     -0.065184  0.923010  ...   -0.455135    1.301029   
m00429.wav    1.580730     -0.006813 -0.074261  ...    0.066440   -0.178251   
m00435.wav    0.862029      2.557347  0.018733  ...   -1.225742   -0.068556   
m00454.wav    0.280223      0.450457  0.250582  ...    0.829423   -0.479910   
m00477.wav    0.028989      1.661682 -1.153990  ...   -0.106110    0.745008   
m00501.wav    1.580730      0.166343 -0.989679  ...   -0.303797    0.357383   
m00503.wav    0.556581      0.046058  1.096817  ...    0.831167   -0.363833   
m00513.wav    0.556581      0.746311  2.555024  ...   -1.817327    1.071247   
m00553.wav    0.028989     -0.064967 -0.095030  ...   -1.059323    1.589195   
m00606.wav   -1.244509      0.299833 -1.080437  ...   -0.406580    0.266873   
m00623.wav   -0.200399     -0.303472  0.707337  ...   -1.526994    1.036150   
m00627.wav    0.280223     -0.543171  0.837107  ...    1.018362   -1.721201   
m00629.wav    0.556581     -0.565524 -0.653461  ...    0.208406   -1.084738   
m00633.wav    0.028989     -0.028764 -0.722448  ...    0.948581   -0.144053   
m00637.wav   -0.410671     -0.634733  0.141796  ...    0.570878   -1.160373   
m00658.wav   -0.604122     -0.833389  0.768728  ...    1.022027   -0.569775   
m00671.wav   -1.833101     -0.275046 -0.438705  ...   -0.195188    1.323956   
m00676.wav    0.028989     -0.610395  0.597952  ...   -1.662574   -0.162367   
m00677.wav    0.028989      3.292053 -1.132697  ...    0.713612   -0.104577   
m00678.wav    1.580730     -0.482588  0.204562  ...    0.893272   -0.170086   
m00716.wav   -0.604122      0.150028 -0.352859  ...    0.416435   -1.028670   
m00762.wav   -0.200399     -0.065397 -0.154784  ...   -0.264746    1.971791   
m00772.wav    0.556581      0.618857  0.354559  ...    0.895781   -0.969128   
m00773.wav   -0.410671     -0.576718 -0.795515  ...   -1.443701    0.858373   
m00801.wav    0.280223     -2.012457  0.026452  ...   -0.878657    0.960988   
m00821.wav    3.043800     -0.273182  2.204145  ...    2.007721   -2.058134   
m00850.wav    0.862029     -0.333645  0.467546  ...   -0.570692   -0.198435   
m00867.wav    0.028989     -0.226568 -0.243652  ...    0.695981   -0.580285   
m00895.wav   -1.101566      0.759804  0.197826  ...   -0.046403    0.823216   
m00996.wav   -0.782691     -0.520240 -0.766709  ...    1.416535   -0.651306   

            mfcc6_mean  mfcc7_mean  mfcc8_mean  mfcc9_mean      genre  \
filename                                                                
m00002.wav    0.173563   -0.548355    0.153848   -0.154394       jazz   
m00039.wav    1.095720   -0.798675    0.182794   -0.490289     reggae   
m00041.wav   -1.459630    0.505158   -1.189411    0.923385        pop   
m00072.wav   -1.291822    0.739165   -0.678809    0.677718      disco   
m00096.wav    0.422015   -0.831156    0.605457   -0.993900      disco   
m00102.wav   -1.321644   -0.031352   -1.639415   -0.359739  classical   
m00112.wav   -0.247635    2.078666    0.398821    1.636401     reggae   
m00138.wav    0.508027    0.056130    1.740265   -0.534648     reggae   
m00192.wav   -0.724115   -0.464254   -1.331074   -0.036760  classical   
m00206.wav    1.742479   -0.575393    1.731972   -0.589271     hiphop   
m00230.wav   -1.017711    0.886396   -0.459603    1.024723    country   
m00236.wav   -1.911773   -0.548167   -1.736999    0.198717  classical   
m00248.wav    1.012103   -1.205427    1.629888   -0.843874      metal   
m00253.wav    1.576968   -1.160275    1.795291   -0.381100      blues   
m00298.wav   -0.155084   -0.290897   -0.230966   -0.845679      blues   
m00313.wav   -0.936220    1.254430   -1.034527    1.018588      blues   
m00338.wav    0.044650   -0.981780    0.763326   -0.323221      blues   
m00339.wav    0.482125   -1.738267    0.525650   -1.817315       rock   
m00351.wav    0.224508   -1.263761    0.097705   -1.010856       jazz   
m00400.wav    0.413194    1.376691   -0.125939    1.418211      blues   
m00421.wav   -0.872056    1.928538   -0.801542    1.730750        pop   
m00429.wav    1.276993   -0.779455    1.302700   -0.201144     hiphop   
m00435.wav    0.070265   -0.406651   -1.356252   -0.684829  classical   
m00454.wav    1.651569   -0.860163    0.866572   -0.001118     hiphop   
m00477.wav   -1.759062    1.014908   -1.072470    1.218380  classical   
m00501.wav   -0.931346    0.920434   -0.964391    0.711141       jazz   
m00503.wav    1.292365   -0.752371    1.663279   -0.991667      metal   
m00513.wav   -0.520090    2.023155   -1.073308    1.984908        pop   
m00553.wav   -0.259077    0.949580   -0.525501    1.473174      disco   
m00606.wav   -1.409103    0.318720   -1.283452    0.108399    country   
m00623.wav   -1.656764    0.225550   -0.769049    0.112965     reggae   
m00627.wav    1.234541   -0.580616    0.712332   -1.005324      metal   
m00629.wav    0.636595   -1.179701    0.734209   -1.312755    country   
m00633.wav    0.452726   -0.776468    0.256885   -0.835520    country   
m00637.wav    1.139184   -0.676071    0.930189    0.200930     hiphop   
m00658.wav    1.303571   -0.934012    1.555147   -0.171742     hiphop   
m00671.wav   -0.597876    1.623375   -0.594650    0.952940     reggae   
m00676.wav   -1.380193    0.640297   -0.997807    0.577001        pop   
m00677.wav    0.204576   -0.210049   -0.148832   -1.572489    country   
m00678.wav    0.980320    0.007298    0.913661   -0.379877      metal   
m00716.wav    0.971441   -0.918776    0.418948   -0.748811       jazz   
m00762.wav    0.197099    1.532570   -0.290346    1.487263      disco   
m00772.wav    0.645363   -0.608964    0.917988   -1.358662       rock   
m00773.wav   -0.891454   -0.316985   -0.201543   -0.652296        pop   
m00801.wav   -0.619027    1.042483   -0.727886    0.569917       rock   
m00821.wav    0.829343   -1.145217    1.029918   -1.187887      metal   
m00850.wav    0.188095    1.320281   -0.975654    0.907961      disco   
m00867.wav    0.747490   -1.185073    0.914528   -1.807509       rock   
m00895.wav   -0.378459    0.749937   -0.676747    0.842742       rock   
m00996.wav   -1.176744    0.574570   -0.955205    1.516457       jazz   

            PCA_Component_1  PCA_Component_2  PCA_Component_3  
filename                                                       
m00002.wav        -0.661286        -3.040738        -3.391739  
m00039.wav         1.359245        -1.304717        -0.430524  
m00041.wav        -3.450563         3.925791         0.799791  
m00072.wav        -2.770486         1.837407         0.168114  
m00096.wav         3.065512         0.807930         1.513794  
m00102.wav        -2.982837        -5.015197         0.836015  
m00112.wav        -3.768145         1.397870        -3.189199  
m00138.wav         1.565384        -0.615252        -1.399014  
m00192.wav        -1.767320        -0.841912         2.762178  
m00206.wav         3.358468         1.573820        -1.208237  
m00230.wav        -2.362658         4.596334         1.185582  
m00236.wav        -2.810388        -4.463865        -1.475698  
m00248.wav         4.470725         1.582211        -0.954290  
m00253.wav         4.767450         1.480511         1.290450  
m00298.wav        -1.134283        -1.986452         1.111878  
m00313.wav        -3.453501        -2.719324        -0.554035  
m00338.wav         3.312428        -0.656204        -0.295714  
m00339.wav         5.259427         0.172304         3.099247  
m00351.wav         1.525518        -1.934687         0.222512  
m00400.wav        -4.155648        -5.060703         0.082468  
m00421.wav        -3.648766         5.153067        -0.633728  
m00429.wav         3.110103         0.484882        -1.841544  
m00435.wav        -0.501535        -3.263213         1.712099  
m00454.wav         3.092246         1.035876        -2.918171  
m00477.wav        -3.621328        -4.559819        -1.722098  
m00501.wav        -1.769586        -0.834441         1.623537  
m00503.wav         4.765493         0.303730        -0.310403  
m00513.wav        -3.595616         6.020670        -2.136228  
m00553.wav        -2.504267         0.584663         0.746404  
m00606.wav        -3.033943        -2.268128         1.297916  
m00623.wav        -2.982673         2.957186         2.197331  
m00627.wav         3.480421         0.665182         0.707633  
m00629.wav         3.505332        -0.797418        -0.518927  
m00633.wav         2.682393        -1.054375         0.450360  
m00637.wav         1.959856         0.237172        -2.565966  
m00658.wav         3.025122         1.473455        -1.712716  
m00671.wav        -2.641565        -0.235871        -2.058647  
m00676.wav        -2.429120         2.636766         1.632049  
m00677.wav         0.558315        -3.583280         1.341483  
m00678.wav         2.223983         0.260151        -1.337563  
m00716.wav         1.520892        -1.279420        -1.250196  
m00762.wav        -3.541632         3.016091        -1.702286  
m00772.wav         4.320138        -1.251159         2.095050  
m00773.wav        -3.113622        -0.409345         3.494737  
m00801.wav        -3.639496         2.947230         2.224247  
m00821.wav         5.771560         2.146561         0.814633  
m00850.wav        -1.917320         1.899693        -0.852695  
m00867.wav         3.686435        -1.322420         0.842992  
m00895.wav        -1.567539         1.639482         1.428211  
m00996.wav        -2.561328        -2.338098        -1.221093  

[50 rows x 34 columns]
In [ ]:
label_group = label.groupby('genre')[['PCA_Component_1', 'PCA_Component_2', 'PCA_Component_3']].agg('mean')
In [ ]:
scdfl = label_group

Met de functie die eerder is gemaakt gaan we kijken naar de afstanden

In [ ]:
# Kijken welke clusters tot welke genre horen
bepaal_genres(df_grouped, euclidische_afstand)
bepaal_genres(df_grouped, manhattan_afstand)
bepaal_genres(df_grouped, minkowski_afstand, 2)
Genres volgens euclidische_afstand:
Cluster 0: {'Genre 1': 'reggae', 'Aantal 1': 1, 'Genre 2': 'pop', 'Aantal 2': 1}
Cluster 1: {'Genre 1': 'metal', 'Aantal 1': 2, 'Genre 2': 'country', 'Aantal 2': 1}
Cluster 2: {'Genre 1': 'classical', 'Aantal 1': 2, 'Genre 2': 'blues', 'Aantal 2': 1}

Genres volgens manhattan_afstand:
Cluster 0: {'Genre 1': 'reggae', 'Aantal 1': 1, 'Genre 2': 'pop', 'Aantal 2': 1}
Cluster 1: {'Genre 1': 'metal', 'Aantal 1': 2, 'Genre 2': 'country', 'Aantal 2': 1}
Cluster 2: {'Genre 1': 'classical', 'Aantal 1': 2, 'Genre 2': 'blues', 'Aantal 2': 1}

Genres volgens minkowski_afstand:
Cluster 0: {'Genre 1': 'reggae', 'Aantal 1': 1, 'Genre 2': 'pop', 'Aantal 2': 1}
Cluster 1: {'Genre 1': 'metal', 'Aantal 1': 2, 'Genre 2': 'country', 'Aantal 2': 1}
Cluster 2: {'Genre 1': 'classical', 'Aantal 1': 2, 'Genre 2': 'blues', 'Aantal 2': 1}
Out[ ]:
{'PCA_Component_1': genre       blues classical   country     disco    hiphop      jazz     metal  \
 cluster                                                                         
 0        0.748973  1.454998  1.151571  0.651955  3.790843  0.492526   5.02412   
 1         5.02322  7.227191  4.620622  6.424148   1.98135  5.279667  0.748073   
 2        2.223338  0.019367  2.625936   0.82241  5.265208  1.966891  6.498485   
 
 genre         pop    reggae      rock  
 cluster                                
 0        2.365853  0.411867  2.493477  
 1        8.138046   6.18406  3.278716  
 2        0.891489  1.062498  3.967842  ,
 'PCA_Component_2': genre       blues classical   country     disco    hiphop      jazz     metal  \
 cluster                                                                         
 0        5.263531  7.103897   4.09647  1.845939  2.514055  5.360573  2.483529   
 1        0.605861  2.446227    0.5612  2.811731  2.143615  0.702903  2.174141   
 2        0.987384  0.852982  2.154446  4.404976   3.73686  0.890342  3.767386   
 
 genre         pop    reggae      rock  
 cluster                                
 0        0.009706  3.035253  3.038009  
 1        4.647964  1.622417  1.619661  
 2        6.241209  3.215662  3.212906  ,
 'PCA_Component_3': genre       blues classical   country     disco    hiphop      jazz     metal  \
 cluster                                                                         
 0         0.49136   0.58685  0.915634  0.139017  1.884976  0.639045  0.051647   
 1        0.524217  0.619707  0.948491  0.171874  1.852119  0.606188   0.01879   
 2        0.026973  0.122463  0.451246  0.325371  2.349363  1.103432  0.516035   
 
 genre         pop    reggae      rock  
 cluster                                
 0        0.795675   0.81166    2.1023  
 1        0.828532  0.778803  2.135157  
 2        0.331287  1.276047  1.637913  }

Uit deze uitkomst kunnen we de genres niet echt bepalen. We gaan het nu op basis van trial en error proberen te achterhalen. In het hoofdstuk hierboven zijn we er achter gekomen dat de genres bestaan uit classical, metal en pop. We weten voor de PCA data nog niet welke kluster hoort bij welk genre, daarom gaan we het op verschillende manieren proberen en uiteindelijk kiezen we voor degene met de beste kaggle score.

In [ ]:
# Maken van dictionary voor vervangen cluster nummers
cluster_map = {
    0 : 'classical',
    1 : 'metal',
    2 : 'pop'
}

# Aanmaken csv voor Kaggle
# kaggle_upload(df, cluster_map, 'submission_cl_me_po_PCA_012')

Deze heeft een accuracy score van 0.20754 op Kaggle dus deze is niet goed. Nu gaan we de volgende proberen.

In [ ]:
# Maken van dictionary voor vervangen cluster nummers
cluster_map = {
    2 : 'classical',
    1 : 'metal',
    0 : 'pop'
}

# Aanmaken csv voor Kaggle
# kaggle_upload(df, cluster_map, 'submission_cl_me_po_PCA_210')

Deze heeft een accuracy score van 0.98113 dus dit is de goede volgorde van de genres.

Verschil in PCA en NMF¶

NMF en PCA zijn beide modellen voor dimensioniteitsvermindering. Ze verminderen beide het aantal kenmerken in een dataset, maar hanteren daarbij verschillende methoden. NMF legt bijvoorbeeld de beperking op dat zowel de basis- als de coëfficiëntenmatrices niet-negatieve waarden moeten hebben, wat betekent dat het bijzonder geschikt is voor gegevens waarin negatieve waarden geen nauwkeurige representatie mogelijk maken, zoals bij afbeeldingen en tekstgegevens. Aan de andere kant accepteert PCA zowel positieve als negatieve waarden.

Een ander onderscheidend kenmerk is dat NMF streeft naar het vinden van additieve, niet-negatieve combinaties van patronen, wat leidt tot onderdeelgebaseerde representaties. Dit is in tegenstelling tot PCA, dat orthogonale assen zoekt (hoofdcomponenten) die de variantie maximaliseren, wat resulteert in ongecorreleerde representaties.

Een ander verschil is dat PCA vaak lastiger te interpreteren is dan NMF, omdat de componenten die uit PCA voortkomen niet direct corresponderen met de kenmerken in de dataset, maar eerder lineaire combinaties zijn van deze kenmerken.

(Benameur, 2023)

Terug naar boven



NMF¶

NMF is een afkorting voor "Non-Negative Matrix Factorisation". NMF is net zoals PCA een manier om dimensionaliteit te reduceren. NMF verschilt van PCA doordat het over het algemeen makkelijker te begrijpen is. Dit komt doordat, zoals de naam zegt, NMF geen negatieve waardes kan bevatten. Dit kan ook een nadeel zijn, omdat het betekent dat NMF niet toe te passen is op datasets die waardes minder dan 0 bevatten. Net zoals PCA principal components heeft, heeft NMF ook componenten.

NMF werkt door het vermenigvuldigen twee matrices, een matrix met de features, en een matrix met de "weights" van elke feature. NMF kan alleen een inschatting maken van de resulterende matrix omdat het probleem niet precies op te lossen is.

Wiskundige uitleg: \begin{align}\begin{aligned}L(W, H) &= 0.5 * ||X - WH||_{loss}^2\\&+ alpha\_W * l1\_ratio * n\_features * ||vec(W)||_1\\&+ alpha\_H * l1\_ratio * n\_samples * ||vec(H)||_1\\&+ 0.5 * alpha\_W * (1 - l1\_ratio) * n\_features * ||W||_{Fro}^2\\&+ 0.5 * alpha\_H * (1 - l1\_ratio) * n\_samples * ||H||_{Fro}^2\end{aligned}\end{align}

$$0.5 \cdot ||X - WH||_{\text{loss}}^2$$

  • In deze term word getest hoe goed het product van matrices W en H de originele data representeert. "loss" representeert een loss functie tussen de originele matrix X en de gereconstrueerde matrix WH.

    $$\alpha_W \cdot \text{l1\_ratio} \cdot \text{n\_features} \cdot ||\text{vec}(W)||_1$$

  • Bij deze term word L1 norm regularisatie toegepast op de gevectoriseerde versie van W. Bij de volgende term gebeurt dit opnieuw maar voor matrix H.

$$0.5 \cdot \alpha_W \cdot (1 - \text{l1\_ratio}) \cdot \text{n\_features} \cdot ||W||_{\text{Fro}}^2$$

  • Met term word matrix regularisatie toegepast op matrix W. De laatste term doet hetzelfde maar dan voor matrix H.
In [ ]:
# Omdat nmf niet gebruikt kan worden met negatieve waardes moeten we eerst zorgen
# dat we alleen maar positieve data hebben. 

# nmfdf = scdf.copy()

# nmfdf[nmfdf < 0] = 0

# Fill NaN values with zero
# nmfdf = nmfdf.fillna(0)

# nmfdf.head()
In [ ]:
# Hier word bepaald hoeveel componenten we gebruiken voor NMF
# components_range = range(1, 10)  
# residual_sums_of_squares = []

# Loop om range van componenten te checken
# for n_components in components_range:
#     nmf_model = NMF(n_components=n_components)
#     nmf_result = nmf_model.fit_transform(nmfdf)
    
#     reconstructed_data = np.dot(nmf_result, nmf_model.components_)
    
#     rss = np.sum((nmfdf - reconstructed_data) ** 2)
    
#     residual_sums_of_squares.append(rss)

# Plot om het optimate aantal componenten te bepalen
# plt.plot(components_range, residual_sums_of_squares, marker='o')
# plt.xlabel('Aantal componenten')
# plt.ylabel('Residual Sum of Squares')
# plt.title('Elleboog methode')
# plt.show()
In [ ]:
# Omdat het dataframe op dit punt nog veranderd, hebben er voor gekozen om een functie aan te maken voor het toepassen van NMF.

def apply_nmf(dataframe, n_components=4, random_state=42):
    """
    Non-Negative Matrix Factorisation toepassen op een dataframe.

    Parameters:
    - dataframe: DataFrame zonder negatieve waardes. 
    - n_components (int): aantal componenten.
    - random_state (int): Seed voor random state.

    Returns:
    - nmf_result (DataFrame): gefactoriseerde output van het originele DataFrame.
    """

    # NMF initialseren
    nmf_model = NMF(n_components=n_components, random_state=random_state)

    # NMF toepassen
    nmf_result = pd.DataFrame(nmf_model.fit_transform(dataframe), index=dataframe.index)

    return nmf_result
In [ ]:
# best_nmf = apply_nmf(data, n_components=4, random_state=42)

bronnen NMF¶

https://iq.opengenus.org/nmf-vs-pca/

https://www.geeksforgeeks.org/non-negative-matrix-factorization/

https://scikit-learn.org/stable/modules/generated/sklearn.decomposition.NMF.html

https://towardsdatascience.com/nmf-a-visual-explainer-and-python-implementation-7ecdd73491f8



Terug naar boven

NMF 2 ¶

NMF staat voor Non-negative Matrix Factorisation. Hierbij is er een erg belangrijk gedeelte Non-negative, aangezien de huidige scaled dataframe met de features negatieve waarden bevat. Om dit te voorkomen zullen we de data van NMF gaan scalen met behulp van de MinMaxScaler.

In [ ]:
# Aanmaken van de scaler
minmax = MinMaxScaler()

# Toepassen van de scaler op unlabeled (behalve eerste kolommen)
scaled_data = minmax.fit_transform(unlabeled.iloc[:, 2:])

# Omzetten van de data naar een dataframe
scaled_df = pd.DataFrame(scaled_data,
                         columns=unlabeled.columns[2:],
                         index=unlabeled.index)

# Tonen van describe om te kijken of het heeft gewerkt
display(scaled_df.describe())
mfcc1_mean mfcc2_mean mfcc3_mean mfcc4_mean mfcc5_mean mfcc6_mean mfcc7_mean mfcc8_mean mfcc9_mean mfcc10_mean ... mean_bandwidth mean_centroids mean_spectral_contrast mean_tonnetz mean_spectral_rolloff mean_rms_energy mean_zcr mean_beat mean_tempo mean_harmonie
count 105.000000 105.000000 105.000000 105.000000 105.000000 105.000000 105.000000 105.000000 105.000000 105.000000 ... 105.000000 105.000000 105.000000 105.000000 105.000000 105.000000 105.000000 105.000000 105.000000 105.000000
mean 0.713325 0.439719 0.399065 0.428874 0.559996 0.463504 0.480816 0.525584 0.450691 0.487541 ... 0.525956 0.495121 0.411253 0.458024 0.502898 0.415691 0.479663 0.332460 0.382754 0.926527
std 0.242423 0.268921 0.240074 0.249618 0.214397 0.225499 0.204837 0.229628 0.246470 0.232694 ... 0.275154 0.268842 0.242931 0.197144 0.275531 0.270905 0.231315 0.083735 0.204204 0.123128
min 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 ... 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
25% 0.499268 0.227978 0.211305 0.242759 0.403624 0.322576 0.318539 0.395461 0.264517 0.349721 ... 0.252340 0.210687 0.208707 0.336822 0.217757 0.127072 0.275954 0.317687 0.240000 0.956587
50% 0.820982 0.378098 0.354820 0.371553 0.573954 0.415137 0.451669 0.467025 0.404343 0.441233 ... 0.517625 0.542856 0.418055 0.441245 0.540074 0.425861 0.485893 0.333108 0.335664 0.961914
75% 0.896399 0.692374 0.617295 0.620374 0.725300 0.552778 0.644463 0.707698 0.649023 0.628641 ... 0.805383 0.691017 0.584579 0.558136 0.750373 0.649673 0.648706 0.347204 0.512821 0.962877
max 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 ... 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000

8 rows × 30 columns

Nu het dataframe is aangemaakt kan de het optimale aantal componenten voor NMF worden bepaald. Dit gebeurt ook door gebruik van de elleboog methode.

In [ ]:
# Aanmaken lijst voor waarden en range voor componenten
errors = []
componenten = range(1, 11)

# Loopen over alle aantallen componenten
for n_components in componenten:
    # Toepassen NMF
    nmf = NMF(n_components=n_components, random_state=42)
    
    # Fit_Transform de data
    nmf.fit_transform(scaled_df)
    
    # Opslaan errors
    errors.append(nmf.reconstruction_err_)

# Plotten van de verschillende errors
plt.plot(componenten, errors, '-x')
plt.xlabel('Aantal componenten')
plt.ylabel('Reconstruction Error')
plt.show()

Het punt waar de lijn het meest op een elleboog lijkt is bij een aantal componenten van 3. Dit is de waarde waarmee we NMF gaan trainen.

In [ ]:
# Toepassen van NMF
nmf = NMF(n_components=3, random_state=42)

# Fit en transformeer de data
nmf_result = nmf.fit_transform(scaled_df)

# Aanmaken van dataframe met de componenten
nmf_df = pd.DataFrame(nmf_result,
                      columns=[f'NMF_Component_{i+1}' for i in range(3)],
                      index=unlabeled.index)

# Tonen van het dataframe
display(nmf_df)
NMF_Component_1 NMF_Component_2 NMF_Component_3
filename
m00003.wav 0.022219 0.538570 0.331300
m00012.wav 0.071243 0.658065 0.157665
m00013.wav 0.000000 0.395245 0.689557
m00043.wav 0.036629 0.530188 0.291035
m00044.wav 0.033812 0.000000 0.631686
... ... ... ...
m00971.wav 0.084712 0.062685 0.564332
m00973.wav 0.383193 0.198367 0.008603
m00988.wav 0.383194 0.186904 0.000000
m00991.wav 0.003947 0.174468 0.862957
m00995.wav 0.000000 0.205393 0.719270

105 rows × 3 columns

Nu we dit dataframe hebben gaan we opnieuw kijken naar de elleboog van KMeans, om te kijken of het aantal clusters is veranderd na het toepassen van NMF.

In [ ]:
# Toepassen elleboog methode
elleboog_KMeans(11, nmf_df)

Nu het optimale aantal clusters is bepaald, namelijk k=3, kan er weer geclustered worden.

In [ ]:
# Toepassen van KMeans_Clustering
nmf_df = clusteren(3, nmf_df)
1    40
0    39
2    26
Name: cluster, dtype: int64

Na het clusteren zien we weer exact dezelfde verdeling als de vorige twee keren, namelijk 40, 39, 26. We zullen nu eerst gaan kijken hoe de clusters zich tonen in een scatterplot.

In [ ]:
# Tonen van de clusters op een 2D-schaal
sns.scatterplot(data=nmf_df,
                x='NMF_Component_2',
                y='NMF_Component_3',
                hue='cluster',
                palette='viridis')

plt.title('NMF cluster verdeling')
plt.show()

Nu er een mooi 2 dimensionale visualisatie is gemaakt van de clusters, kijken we naar een 3 dimensionale visualisatie.

In [ ]:
# Bepalen figuur grootte en assen
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')

# Aanmaken scatterplot met 3 assen
scatter = ax.scatter(nmf_df['NMF_Component_1'],
                     nmf_df['NMF_Component_2'],
                     nmf_df['NMF_Component_3'],
                     c=nmf_df['cluster'],
                     cmap='viridis')

# Aanmaken as-titels en titel
ax.set_xlabel('NMF_Component_1')
ax.set_ylabel('NMF_Component_2')
ax.set_zlabel('NMF_Component_3')
ax.set_title('3D Scatteplot met NMF Clusters')
fig.colorbar(scatter, label='Cluster')

plt.show()

Zoals we kunnen zien zijn er drie mooie clusters aangemaakt door NMF. Nu dit is gedaan kijken we naar de genres die toebehoren aan elk cluster. We weten dat we keuze hebben uit 'metal', 'pop' en 'classical'. Om dit te bereiken passen we NMF toe op de gelabelde dataset, vervolgens visualiseren we de drie eerder genoemde genres in een 2 dimensionaal scatterplot, samen met de NMF clusters.

In [ ]:
# Aanmaken van de scaler
minmax = MinMaxScaler()

# Toepassen van de scaler op unlabeled (behalve eerste kolommen)
scaled_data_label = minmax.fit_transform(labeled.iloc[:, 3:])

# Omzetten van de data naar een dataframe
scaled_df_label = pd.DataFrame(scaled_data_label,
                         columns=labeled.columns[3:],
                         index=labeled.index)

# Tonen van describe om te kijken of het heeft gewerkt
display(scaled_df_label.describe())
mfcc1_mean mfcc2_mean mfcc3_mean mfcc4_mean mfcc5_mean mfcc6_mean mfcc7_mean mfcc8_mean mfcc9_mean mfcc10_mean ... mean_bandwidth mean_centroids mean_spectral_contrast mean_tonnetz mean_spectral_rolloff mean_rms_energy mean_zcr mean_beat mean_tempo mean_harmonie
count 50.000000 50.000000 50.000000 50.000000 50.000000 50.000000 50.000000 50.000000 50.000000 50.000000 ... 50.000000 50.000000 50.000000 50.000000 50.000000 50.000000 50.000000 50.000000 50.000000 50.000000
mean 0.647249 0.487332 0.419637 0.500381 0.478063 0.523164 0.455409 0.491749 0.477961 0.499348 ... 0.505844 0.470589 0.453101 0.386478 0.486691 0.405863 0.449987 0.825163 0.461538 0.472540
std 0.227837 0.204479 0.213392 0.251376 0.234638 0.276432 0.264650 0.285977 0.265674 0.281112 ... 0.207672 0.201221 0.223718 0.188257 0.202795 0.218951 0.214010 0.141550 0.178700 0.152046
min 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 ... 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
25% 0.520977 0.354092 0.282704 0.306407 0.327383 0.280541 0.247423 0.232204 0.263917 0.223888 ... 0.399856 0.358767 0.279424 0.290832 0.395530 0.275181 0.288065 0.815907 0.354667 0.506262
50% 0.718615 0.504105 0.373851 0.512183 0.439452 0.572648 0.375780 0.452855 0.435074 0.473759 ... 0.466852 0.443709 0.464893 0.373760 0.482860 0.377013 0.449679 0.850887 0.466667 0.521117
75% 0.800279 0.603771 0.552962 0.699577 0.654092 0.744517 0.678699 0.729770 0.712470 0.718190 ... 0.604076 0.588606 0.613417 0.464425 0.599470 0.510883 0.569762 0.867246 0.560000 0.522372
max 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 ... 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000

8 rows × 30 columns

Nu gaan we door naar NMF. Omdat de instantie van NMF nog steeds bestaat hoeft deze niet opnieuw te worden aangemaakt. Na het toepassen van NMF kunnen de genres weer aan de data worden toegevoegd. Daarna kan de data op basis van genre worden verkleind, zodat alleen de nodige data overblijft.

In [ ]:
# Fit en transformeer de data
nmf_label = nmf.fit_transform(scaled_df_label)

# Aanmaken van dataframe met de componenten
nmf_df_label = pd.DataFrame(nmf_label,
                      columns=[f'NMF_Component_{i+1}' for i in range(3)],
                      index=labeled.index)

# Toevoegen van de genres aan de data
nmf_df_label['genre'] = labeled['genre']

# Fileteren op de data voor de juiste genres
nmf_label_filtered = nmf_df_label[
    nmf_df_label['genre'].isin(['pop', 'classical', 'metal'])
    ]

# Tonen van het dataframe
display(nmf_label_filtered.head())
NMF_Component_1 NMF_Component_2 NMF_Component_3 genre
filename
m00041.wav 0.007348 0.144511 1.019306 pop
m00102.wav 0.445012 0.000000 0.074475 classical
m00192.wav 0.140916 0.155811 0.432768 classical
m00236.wav 0.484691 0.029504 0.108606 classical
m00248.wav 0.055577 0.754090 0.166697 metal

Nu de data voor beide datasets in orde is kan er gebruik worden gemaakt van de scatterplots.

In [ ]:
vergelijking_genres(
    data1=nmf_df,
    data2=nmf_label_filtered,
    x='NMF_Component_2',
    y='NMF_Component_3'
)

Uit deze grafiek wordt al snel duidelijk bij welk cluster welke genre toebehoord. Met deze informatie kunnen we een bestand aanmaken om op Kaggle in te leveren.

In [ ]:
# Maken van de cluster map
cluster_map = {
    0 : 'classical',
    1 : 'pop',
    2 : 'metal'
}

# Kaggle CSV aanmaken
# kaggle_upload(nmf_df, cluster_map, 'submission_cl_po_me_NMF')

Raar genoeg gaf deze order een score van 0.2. Om toch nog even te kijken naar de verdeling gaan we het 3 dimensionale plot maken van de gelabelde dataset, en deze vergelijken met de dataset van de NMF clusters. Om dit te doen veranderen we eerst de genres naar nummers, aangezien een 3d plot geen genres als kleur indicator aanneemt.

In [ ]:
# Aanmaken genre map
genre_map = {
    'classical' : 0,
    'pop' : 1,
    'metal' : 2
}

# Copy maken van dataset
nmf_label_visu = nmf_label_filtered.copy()

# Aanpassen van genres naar getallen
nmf_label_visu['genre'] = nmf_label_filtered['genre'].replace(genre_map)

# Bepalen figuur grootte en assen
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')

# Aanmaken scatterplot met 3 assen
scatter = ax.scatter(nmf_label_visu['NMF_Component_1'],
                     nmf_label_visu['NMF_Component_2'],
                     nmf_label_visu['NMF_Component_3'],
                     c=nmf_label_visu['genre'],
                     cmap='viridis')

# Aanmaken as-titels en titel
ax.set_xlabel('NMF_Component_1')
ax.set_ylabel('NMF_Component_2')
ax.set_zlabel('NMF_Component_3')
ax.set_title('3D Scatteplot met NMF Genres')
fig.colorbar(scatter, label='Genres')

plt.show()

Ook op de 3 dimensionale schaal liggen de NMF clusters op dezelfde plek als de eerder aangegeven genres. Om deze reden gaan we een klein beetje trial and error toepassen. We draaien als eerste classical en pop om.

In [ ]:
# Maken van de cluster map
cluster_map = {
    0 : 'pop',
    1 : 'classical',
    2 : 'metal'
}

# Kaggle CSV aanmaken
# kaggle_upload(nmf_df, cluster_map, 'submission_po_cl_me_NMF')

Deze volgorde gaf, zoals iets meer verwacht, de juiste uitkomst van 0.98113. Wat de reden hierachter is, is voor ons niet duidelijk.

Terug naar boven

Suggestie App¶

Voordat we verder kunnen zullen eerst de cluster omzetten naar genres. Dit zorgt voor een leesbaarder display van de app.

In [ ]:
# Maken van dictionary voor vervangen cluster nummers
cluster_map = {
    0 : 'classical',
    1 : 'pop',
    2 : 'metal'
}

# Vervangen van clusternummers door genres
scdf['cluster'] = scdf['cluster'].replace(cluster_map)

Een andere opgave die we hadden was om een eenvoudige app te maken voor het aanbevelen van muziek. Om dit te doen is er gekozen om muziek aan te bevelen op basis van het genre. Om hiermee te beginnen moet het lukken om op basis van het genre een audio bestand af te spelen en bestanden aan te raden. Om dit te doen is de onderstaande code ontwikkelt.

In [ ]:
def speel_lied(genre):
    """
    Speelt een willekeurig nummer van het opgegeven muziekgenre af
    en geeft suggesties voor andere nummers binnen hetzelfde genre.

    Parameters:
    ----------
    genre : str
        Het gewenste muziekgenre.

    Returns:
    ----------
    suggesties : list
        Een lijst met suggesties voor andere nummers binnen hetzelfde genre.
        Als het opgegeven genre niet wordt gevonden, wordt None geretourneerd.
    """
    # Selecteer een willekeurig genre
    genre_songs = labeled[labeled['genre'] == genre]

    # Indien het genre niet is gevonden, geef bericht
    if len(genre_songs) == 0:
        print(f"Geen muziek gevonden voor: {genre}."
              f"Probeer een van deze: {labeled['genre'].unique()}")
        return None

    # Pak een willekeurig nummer
    random_song = genre_songs.sample(1).index[0]

    # Display and play the selected song
    audio = os.path.join('labeled/', random_song)
    display(Audio(filename=audio))
    
    # Return suggestions for the selected genre
    suggesties = genre_songs.sample(5)
    return suggesties.index.tolist()

# Selecteren genre en gebruiken functie
selected_genre = 'pop'
suggesties = speel_lied(selected_genre)

# Printen van suggesties
print(f"\nSuggesties voor {selected_genre} genre:")
print(suggesties)
Your browser does not support the audio element.
Suggesties voor pop genre:
['m00513.wav', 'm00041.wav', 'm00773.wav', 'm00421.wav', 'm00676.wav']

Nu dit is gelukt, gaan we ons leven wat lastiger maken. Om te zorgen dat er een handmatig gewisseld kan worden tussen genres en nummers zijn hiervoor knoppen aangemaakt met behulp van de ipywidgets libary.

In [ ]:
def speel_r_lied(genre):
    """
    Speelt een willekeurig nummer van het opgegeven muziekgenre af
    en geeft suggesties voor andere nummers binnen hetzelfde genre.

    Parameters:
    ----------
    genre : str
        Het gewenste muziekgenre.

    Returns:
    ----------
    None
    """
    # Selecteer een willekeurig genre
    genre_songs = labeled[labeled['genre'] == genre]

    # Indien het genre niet is gevonden, geef bericht
    if len(genre_songs) == 0:
        print(f"Geen muziek gevonden voor: {genre}."
              f"Probeer een van deze: {labeled['genre'].unique()}")
        return None

    # Pak een willekeurig nummer
    random_song = genre_songs.sample(1).index[0]

    # Toon bestandsnaam en genre
    with output_area:
        clear_output(wait=True)
        display(HTML(f"<h1>--Genre: {genre}--</h1><h3>{random_song}</h3>"))

        # Tonen en speelbaar maken lied
        audio = os.path.join('labeled/', random_song)
        display(Audio(filename=audio))

    # Plaats suggesties voor het genre
    suggesties = genre_songs.sample(5)
    with output_area:
        print(f"\nSuggesties voor {genre} genre:")
        suggesties_knop = [widgets.Button(description=song, layout=widgets.Layout(width='100%')) for song in suggesties.index]
        for button in suggesties_knop:
            button.on_click(suggestie_knop)
        display(*suggesties_knop)

def suggestie_knop(button):
    """
    Zorgt voor de suggestie knoppen

    Parameters:
    ----------
    button : widgets.Button
        De button voor de suggestie

    Returns:
    ----------
    None
    """
    with output_area:
        speel_g_lied(button.description)

def speel_g_lied(song):
    """
    Afspelen van geselecteerd lied

    Parameters:
    ----------
    song : str
        het lied

    Returns:
    ----------
    None
    """
    # Tonen bestandnaam en genre
    with output_area:
        clear_output(wait=True)
        display(HTML(f"<h1>--Genre: {genre}--</h1><h3>{song}</h3>"))

        # Tonen en afspeelbaar maken lied
        audio = os.path.join('labeled/', song)
        display(Audio(filename=audio))

    # Verversen van suggesties
    suggesties = labeled[labeled['genre'] == genre].sample(5)
    with output_area:
        print(f"\nSuggesties voor {genre} genre:")
        suggesties_knop = [widgets.Button(description=song, layout=widgets.Layout(width='100%')) for song in suggesties.index]
        for button in suggesties_knop:
            button.on_click(suggestie_knop)
        display(*suggesties_knop)

def genre_knop(button):
    """
    Zorgt voor het handelen van de genre knoppen

    Parameters:
    ----------
    button : widgets.Button
        De button voor het genre

    Returns:
    ----------
    None
    """
    with output_area:
        global genre
        genre = button.description
        speel_r_lied(genre)

# Ophalen van genres
genres = labeled['genre'].unique()

# Knop maken voor elk genre
genre_knoppen = [widgets.Button(description=genre) for genre in genres]

# Attach the callback function to button clicks
for button in genre_knoppen:
    button.on_click(genre_knop)

# Maak output area voor lied en suggesties
output_area = widgets.Output()

# Random button
random_button = widgets.Button(description="Random Pick")
random_button.on_click(lambda _: speel_r_lied(random.choice(genres)))

# Tonen resultaat
buttons_box = widgets.HBox(genre_knoppen)
display(buttons_box, output_area, random_button)
HBox(children=(Button(description='jazz', style=ButtonStyle()), Button(description='reggae', style=ButtonStyle…
Output()
Button(description='Random Pick', style=ButtonStyle())

Nu dit is gemaakt kunnen we deze praktijken verder verbeteren en een volledige class maken die functioneert als een app. Deze app heeft als input een dataset, een genre kolom en een directory nodig om te kunnen werken.

In [ ]:
class TuneTips:
    """
    Een class die een app opstart. Deze app kan muziekfragmenten
    afspelen en muziek aanraden op basis van genres.
    """
    def __init__(self, root, dataframe, genre_column, directory):
        """
        Initialisatie van de MusicPlayerApp.

        Parameters:
        ----------
        root : Tk
            Het hoofdvenster van de applicatie.

        dataframe : pandas.DataFrame
            Het DataFrame met informatie over de nummers.

        genre_column : str
            De kolom in het DataFrame die het genre van de nummers bevat.

        directory : str
            Het pad naar de map met de muziekbestanden.
        """
        # Aanmaken venster
        self.root = root
        self.root.title("Tune Tips - Music Advisor (BETA-EDITION)")

        # Initialiseren van mixer
        mixer.init()

        # Gebruiken van doorgegeven df, kolom en dir
        self.labeled = dataframe
        self.genre_column = genre_column
        self.directory = directory

        # Ophalen genres in de genre kolom
        self.available_genres = self.labeled[self.genre_column].unique()

        # Aanmaken van genre knoppen
        self.genre_buttons = [ttk.Button(root,
                                         text=genre,
                                         command=lambda g=genre: self.play_random_song(g)
                                         ) for genre in self.available_genres]
        
        # Aanmaken genre label
        self.genre_label = ttk.Label(root, text="Genres:")
        self.genre_label.grid(row=7, column=3, columnspan=2, sticky="w")

        # Genre knoppen plaatsen
        for i, button in enumerate(self.genre_buttons):
            button.grid(row=8 + i, column=3, sticky="nsew")

        # Maken display window voor tekst
        self.d_window = tk.Text(root, height=12, width=100)
        self.d_window.grid(row=1, column=3, rowspan=6, columnspan=4)
        self.d_window.delete(1.0, tk.END)
        self.d_window.tag_configure("big", font=('Helvetica', 42), justify='center')
        self.d_window.insert(tk.END, f"Kies een genre of klik op\nRandom Song", "big")
        self.d_window.yview(tk.MOVETO, 0.0)
        self.d_window.config(state=tk.DISABLED)

        # Aanmaken suggesties label
        self.suggestions_label = ttk.Label(root, text="Song Suggestions:")
        self.suggestions_label.grid(row=7, column=4, columnspan=2, sticky="w")

        # Aanmaken en plaatsten suggestie knoppen
        self.suggestion_buttons = [ttk.Button(root,
                                              text="",
                                              command=lambda i=i: self.play_selected_song(i),
                                              width=18
                                              ) for i in range(10)]
        
        for i, button in enumerate(self.suggestion_buttons):
            button.grid(row=8 + i, column=4, sticky="w")

        # Volume control
        self.volume_label = ttk.Label(root, text="Volume:")
        self.volume_scale = ttk.Scale(root, from_=0, to=100,
                                      orient=tk.HORIZONTAL,
                                      command=self.set_volume,
                                      length=400)


        self.volume_label.grid(row=9, column=5, sticky="e")
        self.volume_scale.grid(row=9, column=6, sticky="w")
        self.volume_scale.set(50)

        # Play/Pause knop
        self.playing = False
        self.play_pause_button = ttk.Button(root, text="Play",
                                            command=self.play_pause_music)
        
        self.play_pause_button.grid(row=10, column=5,
                                    rowspan=2, columnspan=1, sticky="ew")

        # Restart knop
        self.restart_button = ttk.Button(root, text="Restart",
                                         command=self.restart_music)
        
        self.restart_button.grid(row=10, column=6,
                                 rowspan=2, columnspan=1, sticky="ew")

        # Random Song knop
        self.random_number = ttk.Button(root, text="Random Song",
                                        command=self.random_number)
        
        self.random_number.grid(row=11, column=5,
                                rowspan=2, columnspan=2, sticky="ew")

        # Voortgangs display
        self.progress_label = ttk.Label(root, text="Voortgang:")
        self.progress_var = tk.DoubleVar()
        self.progress_bar = ttk.Progressbar(root, orient=tk.HORIZONTAL,
                                            mode='determinate',
                                            variable=self.progress_var,
                                            length=400)
        
        self.progress_label.grid(row=8, column=5, sticky="e")
        self.progress_bar.grid(row=8, column=6, sticky="w")

        # Updaten elke 100 ms
        self.root.after(100, self.update_progress)

    def update_suggestions(self, genre):
        """
        Update de suggestieknoppen op
        basis van het geselecteerde genre.

        Parameters:
        ----------
        genre : str
            Het geselecteerde genre.
        """
        # Ophalen genre en huidig nummer
        genre_songs = self.labeled[self.labeled[self.genre_column] == genre]
        random_song = self.current_song
        self.current_genre = genre

        # Geven van suggesties op basis van gelijkenis
        suggestions = self.get_unique_suggestions(genre_songs, random_song, 10)
        suggestions_sorted = self.sort_suggestions_by_similarity(suggestions)
        for i, song in enumerate(suggestions_sorted):
            self.suggestion_buttons[i].config(text=song)

    def sort_suggestions_by_similarity(self, suggestions):
        """
        Sorteer de suggesties op basis van
        cosinusgelijkenis met het huidige nummer.

        Parameters:
        ----------
        suggestions : list of str
            Een lijst met nummers als suggesties.

        Returns:
        ----------
        sorted_suggestions : list of str
            Een lijst met nummers gesorteerd op cosinusgelijkenis.
        """
        # Bereken de features voor het huidige nummer
        current_song_features = self.get_song_features(self.current_song)

        # Bereken de cosinusgelijkenis met elk nummer
        similarity_scores = []
        for song in suggestions:
            song_features = self.get_song_features(song)
            similarity_score = cosine_similarity(
                [current_song_features], [song_features]
                )[0][0]
            similarity_scores.append(similarity_score)

        # Sorteer de suggesties op basis van de cosinusgelijkenis
        sorted_suggestions = [song for _, song in sorted(
            zip(similarity_scores, suggestions), reverse=True
            )]

        return sorted_suggestions

    def get_song_features(self, song):
        """
        Haal de features van het
        nummer op uit het DataFrame.

        Parameters:
        ----------
        song : str
            Het nummer waarvan de
            features moeten worden opgehaald.

        Returns:
        ----------
        features : numpy array
            Een array met de features van het nummer.
        """
        # Selecteren van bruikbare features
        features = self.labeled.loc[song, self.labeled.columns != 'cluster'].values
        return features

    def play_random_song(self, genre):
        """
        Speel een willekeurig nummer af van het opgegeven genre.

        Parameters:
        ----------
        genre : str
            Het genre van de nummers om uit te kiezen.
        """
        # Indien er muziek speelt, update alleen de buttons
        if self.playing:
            self.update_suggestions(genre)
            return
        
        # Ophalen genres en muziek
        genre_songs = self.labeled[self.labeled[self.genre_column] == genre]
        random_song = genre_songs.sample(1).index[0]

        # Tonen van bestand en genre
        self.d_window.config(state=tk.NORMAL)
        self.d_window.delete(1.0, tk.END)
        self.d_window.tag_configure("big", font=('Helvetica', 42), justify='center')
        self.d_window.insert(tk.END, f"Currently playing: {random_song}"
                                f"\n--Genre: {genre}--", "big")
        
        self.d_window.yview(tk.MOVETO, 0.0)
        self.d_window.config(state=tk.DISABLED)

        # Opslaan van het genre en nummer
        self.current_genre = genre
        self.current_song = random_song

        # Speel het bestand af
        file_path = os.path.join(self.directory, random_song)
        mixer.music.load(file_path)
        mixer.music.play()

        # Geef suggesties op basis van genre
        suggestions = self.get_unique_suggestions(genre_songs, random_song, 10)
        for i, song in enumerate(suggestions):
            self.suggestion_buttons[i].config(text=song)

        # Zet voortgangsbar aan
        song_length = mixer.Sound(file_path).get_length()
        self.progress_bar["maximum"] = int(song_length)

        # Update status
        self.playing = True
        self.play_pause_button.config(text="Pause")

    def get_unique_suggestions(self, genre_songs, current_song, num_suggestions):
        """
        Genereer unieke suggesties voor het geselecteerde genre.

        Parameters:
        ----------
        genre_songs : pandas.DataFrame
            DataFrame met nummerinformatie van het geselecteerde genre.

        current_song : str
            Het nummer dat momenteel wordt afgespeeld.

        num_suggestions : int
            Het aantal suggesties dat moet worden gegenereerd.

        Returns:
        ----------
        suggestions : list of str
            Een lijst met unieke suggesties.
        """
        # Excludeer huidig nummer
        genre_songs = genre_songs[genre_songs.index != current_song]

        # Haal suggesties op
        suggestions = []
        while len(suggestions) < num_suggestions and not genre_songs.empty:
            song = genre_songs.sample(1).index[0]
            suggestions.append(song)
            genre_songs = genre_songs[genre_songs.index != song]

        return suggestions

    def play_selected_song(self, index):
        """
        Speel het geselecteerde nummer af op
        basis van de index van de suggestieknop.

        Parameters:
        ----------
        index : int
            De index van de suggestieknop.
        """
        # Haal nummer op, op basis van knop
        selected_song = self.suggestion_buttons[index].cget("text")
        file_path = os.path.join(self.directory, selected_song)
        mixer.music.load(file_path)
        mixer.music.play()

        # Updaten display tekst
        self.d_window.config(state=tk.NORMAL)
        self.d_window.delete(1.0, tk.END)
        self.d_window.tag_configure("big", font=('Helvetica', 42), justify='center')
        self.d_window.insert(tk.END, f"Currently playing: {selected_song}"
                             f"\n--Genre: {self.current_genre}--", "big")
        
        self.d_window.yview(tk.MOVETO, 0.0)
        self.d_window.config(state=tk.DISABLED)

        # Opslaan van nummer
        self.current_song = selected_song

        # Update status
        self.playing = True
        self.play_pause_button.config(text="Pause")

    def random_number(self):
        """
        Speel een willekeurig nummer
        af van een willekeurig genre.
        """
        # Kiezen willekeurig genre
        genre = random.choice(self.available_genres)
        self.play_random_song(genre)

    def restart_music(self):
        """
        Herstart het afspelen van het huidige nummer.
        """
        # Indien er muziek speelt
        if self.playing:
            mixer.music.rewind()
            mixer.music.play()
        # Begin opnieuw wanneer op pauze 
        # (zonder code wilde dit niet werken)
        else:
            file_path = os.path.join(self.directory, self.current_song)
            mixer.music.load(file_path)
            mixer.music.play()

        # Updaten status
        self.playing = True
        self.play_pause_button.config(text="Pause")

    def set_volume(self, val):
        """
        Stel het volume in op basis van de opgegeven waarde.

        Parameters:
        ----------
        val : float
            De waarde om het volume in te
            stellen (tussen 0 en 100).
        """
        # Regelen van volumeknop
        volume = float(val) / 100
        mixer.music.set_volume(volume)

    def play_pause_music(self):
        """
        Pauzeer of hervat het afspelen van muziek
        """
        # Als muziek speelt, pauzeer
        if mixer.music.get_busy():
            mixer.music.pause()
            self.play_pause_button.config(text="Play")
        # Zo niet, speel muziek
        else:
            mixer.music.unpause()
            self.play_pause_button.config(text="Pause")

        # Wissel van staat
        self.playing = not self.playing

    def update_progress(self):
        """
        Voortgangsbalk op basis
        van de huidige afspeeltijd.
        """
        # Indien muziek speelt
        if mixer.music.get_busy():
            current_time = mixer.music.get_pos() / 1000
            self.progress_var.set(current_time)
        else:
            # Als muziek klaar is, zet knop op tekst Play
            self.play_pause_button.config(text="Play")
            self.playing = False

        # update elke 100 ms
        self.root.after(100, self.update_progress)

Nu de class is gemaakt kan deze worden aangeroepen. Zodra dit gebeurt opent er een extra window. Deze window bevat knoppen waarop gedrukt kan worden om handelingen uit te voeren binnen de applicatie. De eerste cel is in comments gezet zodat deze niet zal runnen bij run-all. Dit is voornamelijk omdat we liever hebben dat de app ervaren wordt met de geclusterde data.

In [ ]:
# Gebruiken van de app op labeled
# root = tk.Tk()
# app = MusicPlayerApp(root, labeled, 'genre', 'labeled/')
# root.mainloop()
In [ ]:
# Gebruiken van de app op unlabeled
root = tk.Tk()
app = TuneTips(root, scdf, 'cluster', 'unlabeled/')
root.mainloop()

Terug naar boven

Bevindingen en conclusie¶

Om te bepalen wat de belangrijkste features zijn in het clustering algoritme, wordt een Random Forest model getrained. Door dit model te trainen kunnen we kijken naar welke features het meetste aangeven over het kiezen van genres. Om hieraan te beginnen gaan we de data splitsen in een train en een test dataset.

In [ ]:
# Selecteren van target
target = 'cluster'

# Making the SEED
SEED = 42

# Splitting the data into X and y
X = scdf.drop(target, axis=1)
y = scdf[target]

# Applying train_test_split to get train and test data
X_train, X_test, y_train, y_test = train_test_split(
                                        X, y, test_size=0.2, random_state=SEED
                                        )

Nu de data is gesplitst in een train en een test set kan het model worden getrained. Dit wordt gedaan met een paar parameters om overfitting te voorkomen.

In [ ]:
# Uitvoeren van random forest
rf = RandomForestClassifier(
    max_depth=10,
    n_estimators=100,
    min_samples_leaf=2,
    n_jobs=-1,
    random_state=SEED
)

# Fitten van de data in het model
rf.fit(X_train, y_train)

# Voorspellen van de uitkomsten
y_pred = rf.predict(X_test)

Nu het model heeft voorspeld is het mogelijk om naar de feature importances te kijken en deze te visualiseren.

In [ ]:
# Aanmaken en sorteren invloed kolommen
invloed = pd.Series(rf.feature_importances_, index=X.columns)
invloed = invloed.sort_values(ascending=True)

# Plotten grafiek met invloed
invloed.plot(kind='barh', figsize=(10, 20))
plt.ylabel('Features')
plt.xlabel('Invloed')
plt.show()

In de grafiek is te zien dat volgens het Random Forest model, de gemiddelde bandbreedte het meeste invloed heeft gehad op de bepalen van de genres in de dataset. Dit geeft indirect ook aan dat de bandbreedte het meest doorslaggevend is geweest bij het maken van de clusters, aangezien het supervised learning model hierin de beste patronen kan herkennen. Naast de bandbreedte zijn ook een aantal van de MFCC features, de spectral rolloff en de centroids meer van belang geweest voor het voorspellen.

Aan de andere kant van de grafiek is te zien dat harmonie, tempo en beat eigenlijk weinig indicatie gaven over het genre van de clusters. Tussen deze waarden zitten ook een aantal MFCC waarden, dit geeft duidelijk aan dat niet alle MFCC's even belangrijk zijn of evenveel impact hebben op het voorspellen van de clusters. Met een gecombineerd aantal van 20 MFCC waarden, is het niet apart dat er een aantal met hoge invloed en een aantal met lage invloed aanwezig zijn.

Naast deze standaard features is er ook gebruik gemaakt van dimensionaliteits reductie, om een andere kijk op de features te krijgen. Hoewel we in onze scores op Kaggle geen verschil zien, zijn er wel voordelen van het gebruiken van deze technieken. Het eerste voor is dat het model minder complex wordt, aangezien er minder features aanwezig zijn. Daarnaast zorgt het gebruik van PCA of NMF ook voor een snellere trainings tijd, dit komt door dezelfde redenen. Minder input data geeft een snellere output omdat er minder patronen zijn om over na te denken. Ook helpt het gebruik van PCA of NMF met het verwijderen van eventuele ruis in de data, waardoor er minder kans is op overfitting. Door de ruis te behandelen wordt ook enige overbodige informatie verwijderd. (Simplilearn, 2023)

In onze opdracht zouden we echter kiezen om toch voor de oorspronkelijke data te gaan. Aangezien deze data meteen de clusters op de juiste plekken wist te plaatsen, anders dan bij NMF. Daarnaast zijn er nog wel mogelijkheden om het model te verbeteren. Deze mogelijkheden liggen voornamelijk in het vinden en doorspitten van meer data. Naar onze mening zouden deze vier punten een goede impact kunnen hebben:

  • Data over de artiest
    • Met data over de artiest kun je zien wat voor nummer hij/zij nog meer heeft gemaakt waardoor je zijn stijl kan achterhalen en vaak maakt een artiest wel veel nummers in het zelfde genre.
  • Tekst analyse van de songteksten
    • Aan de hand van de tekst kun je heel veel zeggen waar het nummer over gaat. De boodschap van het nummer die wordt overgedragen met de songtekst kan veel zeggen over welk genre het nummer heeft.
  • Volledige nummers
    • Nu hebben we maar 30 seconden van een nummer. Als dit de eerste 30 seconden zijn van het nummer hoeft dat niet te betekenen dat het hele nummer zo gaat, misschien is het op het begin wel heel rustig en wordt het daarna een veel drukker nummer. Als je het volledige nummer hebt kun je kijken wat over het hele nummer het hoofd genre is, zeker als het refrein ook bij de data zit.
  • Instrumentatiegegevens
    • De verschillende genres hebben verschillende instrumenten die centraal staan. Bij rock is dat bijvoorbeel een gitaar. Als we de gevens hebben van welke instrumenten er in het nummer zitten kunnen we het genre nog beter voorspellen.

Aan de hand van sommige punten zou het niet alleen mogelijk moeten zijn om de voorspellingen meer aan te scherpen, maar het zou ook een mogelijkheid geven om nog verder te kijken. Zo kent een genre zoals metal verschillende subgenres. Deze zouden aan de hand van tekst-analyse, de volledige audiofragmenten en instumentatie beter te onderscheiden moeten zijn.

Terug naar boven

Bronnen¶

Brilliant Math & Science Wiki. (n.d.). Discrete Fourier Transform.
https://brilliant.org/wiki/discrete-fourier-transform/

Benameur, A. (2023, 30 augustus). Non-negative Matrix Factorization (NMF) vs Principal Component Analysis (PCA). OpenGenus IQ: Computing Expertise & Legacy.
https://iq.opengenus.org/nmf-vs-pca/

Hoorn.be - Muziektermen. (z.d.).
http://www.hoorn.be/muziektermen.htm

Jaadi, Z. (2023, 29 maart). A Step-by-Step Explanation of Principal Component Analysis (PCA). Built In.
https://builtin.com/data-science/step-step-explanation-principal-component-analysis

Kumar, N. (2021, 14 december). Unsupervised learning — Principal Component Analysis (PCA). Medium.
https://medium.com/analytics-vidhya/unsupervised-learning-principal-component-analysis-pca-dc94fecef09b

Krish Naik. (2018, 2 juli). Principle Component Analysis (PCA) using Sklearn and Python [Video]. YouTube.
https://www.youtube.com/watch?v=QdBy02ExhGI

Librosa.beat.beat_track — Librosa 0.10.1 Documentation. (z.d.).
https://librosa.org/doc/main/generated/librosa.beat.beat_track.html

Librosa.effects.hPss — Librosa 0.10.1 Documentation. (z.d.).
https://librosa.org/doc/main/generated/librosa.effects.hpss.html

librosa.feature.spectral_rolloff — librosa 0.10.1 documentation. (n.d.).
https://librosa.org/doc/main/generated/librosa.feature.spectral_rolloff.html

librosa.feature.tonnetz — librosa 0.10.1 documentation. (n.d.).
https://librosa.org/doc/main/generated/librosa.feature.tonnetz.html

Medium. (2021, December 10). MFCC’s Made Easy - Tanveer Singh.
https://medium.com/@tanveer9812/mfccs-made-easy-7ef383006040

Nam, U. (2001, April 28). Special Area Exam Part II.
https://ccrma.stanford.edu/~unjung/AIR/areaExam.pdf

Simplilearn. (2023, 7 november). What is dimensionality reduction? overview, and popular techniques.
https://www.simplilearn.com/what-is-dimensionality-reduction-article

Singh, T. (2021, December 10). MFCC’s Made Easy - Tanveer Singh - medium. Medium.
https://medium.com/@tanveer9812/mfccs-made-easy-7ef383006040

spectral_features. (n.d.).
https://musicinformationretrieval.com/spectral_features.html

StatQuest with Josh Starmer. (2018, 8 januari). StatQuest: PCA in Python [Video]. YouTube.
https://www.youtube.com/watch?v=Lsue2gEM9D0

Tempo - muziektheorie betekenis en uitleg tempo. (z.d.).
https://tweedehands-gitaar.nl/muziektheorie/begrippen/tempo

Wikipedia contributors. (2023, November 13). Mel-frequency cepstrum.
https://en.wikipedia.org/wiki/Mel-frequency_cepstrum

Terug naar boven